From 985c984344e0faa5ef4f48902becf327db2407fb Mon Sep 17 00:00:00 2001 From: santinoncs Date: Mon, 27 Jul 2020 15:02:41 +0200 Subject: [PATCH] Initial release (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Santiago Núñez-Cacho Co-authored-by: Riccardo Piccoli --- .gitignore | 5 + Dockerfile | 15 + Makefile | 106 ++++ README.md | 87 ++++ api/v1alpha1/groupversion_info.go | 36 ++ api/v1alpha1/nodepolicyprofile_types.go | 63 +++ api/v1alpha1/zz_generated.deepcopy.go | 130 +++++ chart/node-policy-webhook/.helmignore | 23 + chart/node-policy-webhook/Chart.yaml | 6 + chart/node-policy-webhook/templates/NOTES.txt | 1 + .../templates/_helpers.tpl | 70 +++ .../templates/clusterrolebinding.yaml | 12 + .../templates/deployment.yaml | 72 +++ .../templates/mutatingwebhook.yaml | 30 ++ .../nodepolicyprofile_viewer_role.yaml | 20 + ...licies.softonic.io_nodepolicyprofiles.yaml | 287 +++++++++++ .../node-policy-webhook/templates/secret.yaml | 10 + .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 10 + chart/node-policy-webhook/values.yaml | 67 +++ .../node-policy-webhook.go | 131 +++++ go.mod | 15 + go.sum | 455 ++++++++++++++++++ hack/boilerplate.go.txt | 15 + hack/generate_helm_cert_secrets | 101 ++++ pkg/admission/fetcher.go | 42 ++ pkg/admission/patch.go | 105 ++++ pkg/admission/patch_test.go | 379 +++++++++++++++ pkg/admission/reviewer.go | 116 +++++ pkg/admission/reviewer_test.go | 206 ++++++++ pkg/http/handler.go | 75 +++ pkg/log/level.go | 10 + pkg/version/version.go | 5 + samples/nodepolicyprofile.yaml | 21 + ssl/csr.conf | 14 + 35 files changed, 2755 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 api/v1alpha1/groupversion_info.go create mode 100644 api/v1alpha1/nodepolicyprofile_types.go create mode 100644 api/v1alpha1/zz_generated.deepcopy.go create mode 100644 chart/node-policy-webhook/.helmignore create mode 100644 chart/node-policy-webhook/Chart.yaml create mode 100644 chart/node-policy-webhook/templates/NOTES.txt create mode 100644 chart/node-policy-webhook/templates/_helpers.tpl create mode 100644 chart/node-policy-webhook/templates/clusterrolebinding.yaml create mode 100644 chart/node-policy-webhook/templates/deployment.yaml create mode 100644 chart/node-policy-webhook/templates/mutatingwebhook.yaml create mode 100644 chart/node-policy-webhook/templates/nodepolicyprofile_viewer_role.yaml create mode 100644 chart/node-policy-webhook/templates/noodepolicies.softonic.io_nodepolicyprofiles.yaml create mode 100644 chart/node-policy-webhook/templates/secret.yaml create mode 100644 chart/node-policy-webhook/templates/service.yaml create mode 100644 chart/node-policy-webhook/templates/serviceaccount.yaml create mode 100644 chart/node-policy-webhook/values.yaml create mode 100644 cmd/node-policy-webhook/node-policy-webhook.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/boilerplate.go.txt create mode 100755 hack/generate_helm_cert_secrets create mode 100644 pkg/admission/fetcher.go create mode 100644 pkg/admission/patch.go create mode 100644 pkg/admission/patch_test.go create mode 100644 pkg/admission/reviewer.go create mode 100644 pkg/admission/reviewer_test.go create mode 100644 pkg/http/handler.go create mode 100644 pkg/log/level.go create mode 100644 pkg/version/version.go create mode 100644 samples/nodepolicyprofile.yaml create mode 100644 ssl/csr.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0335090 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +ssl/*.csr +ssl/*.key +ssl/*.pem +manifest.yaml +chart/node-policy-webhook/secret.values.yaml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7e66d6c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.14-buster AS build + +ENV GOBIN=$GOPATH/bin + +ADD . /src/node-policy-webhook + +WORKDIR /src/node-policy-webhook + +RUN make build + +FROM debian:buster-slim + +COPY --from=build /src/node-policy-webhook/node-policy-webhook /node-policy-webhook + +ENTRYPOINT ["/node-policy-webhook"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..215f9ee --- /dev/null +++ b/Makefile @@ -0,0 +1,106 @@ +BIN := node-policy-webhook +CRD_OPTIONS ?= "crd:trivialVersions=true" +PKG := github.com/softonic/node-policy-webhook +VERSION := 0.1.0-dev +ARCH := amd64 +APP := node-policy-webhook +NAMESPACE := default +RELEASE_NAME := node-policy-webhook +KO_DOCKER_REPO = registry.softonic.io/node-policy-webhook + +IMAGE := $(BIN) + +BUILD_IMAGE ?= golang:1.14-buster + + +deploy-prod: export IMAGE_GEN = "github.com/softonic/node-policy-webhook/cmd/node-policy-webhook" + +deploy: export IMAGE_GEN = $(APP):$(VERSION) + + +.PHONY: all +all: dev + +.PHONY: build +build: generate + go mod download + GOARCH=${ARCH} go build -ldflags "-X ${PKG}/pkg/version.Version=${VERSION}" ./cmd/node-policy-webhook/.../ + +.PHONY: test +test: + GOARCH=${ARCH} go test -v -ldflags "-X ${PKG}/pkg/version.Version=${VERSION}" ./... + +.PHONY: image +image: + docker build -t $(IMAGE):$(VERSION) -f Dockerfile . + docker tag $(IMAGE):$(VERSION) $(IMAGE):latest + +.PHONY: dev +dev: image + kind load docker-image $(IMAGE):$(VERSION) + +.PHONY: undeploy +undeploy: + kubectl delete -f manifest.yaml || true + +.PHONY: deploy +deploy: manifest + kubectl apply -f manifest.yaml + +.PHONY: up +up: image undeploy deploy + +.PHONY: docker-push +docker-push: + docker push $(IMAGE):$(VERSION) + docker push $(IMAGE):latest + +.PHONY: version +version: + @echo $(VERSION) + +.PHONY: secret-values +secret-values: + ./hack/generate_helm_cert_secrets $(APP) $(NAMESPACE) + +.PHONY: manifest +manifest: controller-gen helm-chart secret-values + docker run --rm -v $(PWD):/app -w /app/ alpine/helm:3.2.3 template --release-name $(RELEASE_NAME) --set "image.tag=$(VERSION)" -f chart/node-policy-webhook/values.yaml -f chart/node-policy-webhook/secret.values.yaml chart/node-policy-webhook > manifest.yaml + +.PHONY: helm-chart +helm-chart: controller-gen + $(CONTROLLER_GEN) $(CRD_OPTIONS) webhook paths="./..." output:crd:artifacts:config=chart/node-policy-webhook/templates + +.PHONY: helm-deploy +helm-deploy: helm-chart secret-values + helm upgrade --install $(RELEASE_NAME) --namespace $(NAMESPACE) --set "image.tag=$(VERSION)" -f chart/node-policy-webhook/values.yaml -f chart/node-policy-webhook/secret.values.yaml chart/node-policy-webhook + +.PHONY: generate +generate: controller-gen + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +# Run go fmt against code +fmt: + go fmt ./... + +# Run go vet against code +vet: + go vet ./... + +# find or download controller-gen +# download controller-gen if necessary +.PHONY: controller-gen +controller-gen: +ifeq (, $(shell which controller-gen)) + @{ \ + set -e ;\ + CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ + cd $$CONTROLLER_GEN_TMP_DIR ;\ + go mod init tmp ;\ + go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.5 ;\ + rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ + } +CONTROLLER_GEN=$(GOBIN)/controller-gen +else +CONTROLLER_GEN=$(shell which controller-gen) +endif diff --git a/README.md b/README.md index 20f032c..32d7048 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,89 @@ # node-policy-webhook K8s webhook handling profiles for tolerations, nodeSelector and nodeAffinity + + +## DEVEL ENVIRONMENT + +### Requirements + +Install kind + +```bash +curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.8.1/kind-$\(uname\)-amd64 +mv kind-darwin-amd64 /usr/local/bin/kind +``` + + +```bash + +make dev +make deploy +``` + +Now you can run a pod to test it + + +# Motivation + + +The goal of Node Policy Webhook is to reduce Kubernetes manifests complexity by +moving the logic behind the scheduling ( when assigning pods to nodes ) +to a unique place where the cluster's operator/admin can handle it. + +This is accomplished with a new mutating webhook and new CRD where you can place all the node scheduling intelligente +in the form of profiles. + +In this new CRD, you can set different "profiles" depending on the nodes or VMs provision on your cluster. + +When doing that, you can remove all the tolterations,nodeSelectors and nodeAffinities from your +manifests' workloads ( like Deployments, DaemonSets, StatefulSets ..) + +If you are running Knative in your cluster, this project can help you. At the time this is being writen, +there is no way you can schedule knative workloads ( in the form of ksvc ) to a desired node or vm +as it does not implement tolerations neither nodeSelectors. + + +Example: + +You hace an specific deployment, but you'd like these pods to be scheduled in nodes with label disk=ssd + +So first step is to create an object of type + +``` +apiVersion: noodepolicies.softonic.io/v1alpha1 +kind: NodePolicyProfile +metadata: + name: ssd +spec: + nodeSelector: + disk: "ssd" +``` + + +Now you just need to deploy these pods setting this annotation in your deployment + +``` +nodepolicy.softonic.io/profile: "ssd" +``` + +In deployment time, the mutating webhook will replace the nodeSelector with the nodeSelector above mentioned. + + +### Caveats + + +* If your workload already has a nodeSelector defined, mutating webhook will remove it. +* If your workload already has tolerations defined, mutating webhook will keep them. +* If your workload already has Affinities defined, it will keep the podAntiAffinities and podAffinities +and will remove the nodeAffinities + + + + + +``` +Assigning pods to nodes: + +You can constrain a Pod to only be able to run on particular Node(s), or to prefer to run on particular nodes. There are several ways to do this, and the recommended approaches all use label selectors to make the selection. Generally such constraints are unnecessary, as the scheduler will automatically do a reasonable placement (e.g. spread your pods across nodes, not place the pod on a node with insufficient free resources, etc.) but there are some circumstances where you may want more control on a node where a pod lands, for example to ensure that a pod ends up on a machine with an SSD attached to it, or to co-locate pods from two different services that communicate a lot into the same availability zone. + +``` diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..d5b03c9 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the noodepolicies v1 API group +// +kubebuilder:object:generate=true +// +groupName=noodepolicies.softonic.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "noodepolicies.softonic.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha1/nodepolicyprofile_types.go b/api/v1alpha1/nodepolicyprofile_types.go new file mode 100644 index 0000000..38554d6 --- /dev/null +++ b/api/v1alpha1/nodepolicyprofile_types.go @@ -0,0 +1,63 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// NodePolicyProfileSpec defines the desired state of NodePolicyProfile +type NodePolicyProfileSpec struct { + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + NodeAffinity corev1.NodeAffinity `json:"nodeAffinity,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` +} + +// NodePolicyProfileStatus defines the observed state of NodePolicyProfile +type NodePolicyProfileStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster + +// NodePolicyProfile is the Schema for the nodepolicyprofiles API +type NodePolicyProfile struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NodePolicyProfileSpec `json:"spec,omitempty"` + Status NodePolicyProfileStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// NodePolicyProfileList contains a list of NodePolicyProfile +type NodePolicyProfileList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NodePolicyProfile `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NodePolicyProfile{}, &NodePolicyProfileList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..9653449 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,130 @@ +// +build !ignore_autogenerated + +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePolicyProfile) DeepCopyInto(out *NodePolicyProfile) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePolicyProfile. +func (in *NodePolicyProfile) DeepCopy() *NodePolicyProfile { + if in == nil { + return nil + } + out := new(NodePolicyProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodePolicyProfile) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePolicyProfileList) DeepCopyInto(out *NodePolicyProfileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NodePolicyProfile, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePolicyProfileList. +func (in *NodePolicyProfileList) DeepCopy() *NodePolicyProfileList { + if in == nil { + return nil + } + out := new(NodePolicyProfileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodePolicyProfileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePolicyProfileSpec) DeepCopyInto(out *NodePolicyProfileSpec) { + *out = *in + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.NodeAffinity.DeepCopyInto(&out.NodeAffinity) + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePolicyProfileSpec. +func (in *NodePolicyProfileSpec) DeepCopy() *NodePolicyProfileSpec { + if in == nil { + return nil + } + out := new(NodePolicyProfileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePolicyProfileStatus) DeepCopyInto(out *NodePolicyProfileStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePolicyProfileStatus. +func (in *NodePolicyProfileStatus) DeepCopy() *NodePolicyProfileStatus { + if in == nil { + return nil + } + out := new(NodePolicyProfileStatus) + in.DeepCopyInto(out) + return out +} diff --git a/chart/node-policy-webhook/.helmignore b/chart/node-policy-webhook/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/chart/node-policy-webhook/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/chart/node-policy-webhook/Chart.yaml b/chart/node-policy-webhook/Chart.yaml new file mode 100644 index 0000000..8e86dc9 --- /dev/null +++ b/chart/node-policy-webhook/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: node-policy-webhook +description: Apply profiles of tolerations, nodeSelector and nodeAffinities by webhook +type: application +version: 0.1.0 +appVersion: 0.1.0 diff --git a/chart/node-policy-webhook/templates/NOTES.txt b/chart/node-policy-webhook/templates/NOTES.txt new file mode 100644 index 0000000..e49ca9c --- /dev/null +++ b/chart/node-policy-webhook/templates/NOTES.txt @@ -0,0 +1 @@ +Node Policy Webhook Release {{ .Release.Name }} installed in Namespace {{ .Release.Namespace }} \ No newline at end of file diff --git a/chart/node-policy-webhook/templates/_helpers.tpl b/chart/node-policy-webhook/templates/_helpers.tpl new file mode 100644 index 0000000..e055c4f --- /dev/null +++ b/chart/node-policy-webhook/templates/_helpers.tpl @@ -0,0 +1,70 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "node-policy-webhook.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "node-policy-webhook.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "node-policy-webhook.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "node-policy-webhook.labels" -}} +helm.sh/chart: {{ include "node-policy-webhook.chart" . }} +{{ include "node-policy-webhook.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "node-policy-webhook.selectorLabels" -}} +app.kubernetes.io/name: {{ include "node-policy-webhook.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "node-policy-webhook.serviceAccountName" -}} +{{ default (include "node-policy-webhook.fullname" .) .Values.serviceAccount.name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "node-policy-webhook.bindAddress" -}} +{{- if .Values.bindAddress -}} +{{- .Values.bindAddress | quote }} +{{- else }} +{{- printf "%s:%s" (default "0.0.0.0" .Values.bindHost) (toString .Values.bindPort) }} +{{- end }} +{{- end -}} diff --git a/chart/node-policy-webhook/templates/clusterrolebinding.yaml b/chart/node-policy-webhook/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..7d0a927 --- /dev/null +++ b/chart/node-policy-webhook/templates/clusterrolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: nodepolicyprofile-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: nodepolicyprofile-viewer-role +subjects: +- kind: ServiceAccount + name: {{ include "node-policy-webhook.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/chart/node-policy-webhook/templates/deployment.yaml b/chart/node-policy-webhook/templates/deployment.yaml new file mode 100644 index 0000000..de692d7 --- /dev/null +++ b/chart/node-policy-webhook/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "node-policy-webhook.fullname" . }} + labels: + {{- include "node-policy-webhook.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "node-policy-webhook.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + app: node-policy-webhook + {{- include "node-policy-webhook.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "node-policy-webhook.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + args: + - --tls-cert={{ .Values.certs.mountPath }}/cert.pem + - --tls-key={{ .Values.certs.mountPath }}/key.pem + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: BIND_ADDRESS + value: {{ include "node-policy-webhook.bindAddress" . | quote }} + ports: + - name: https + containerPort: {{ .Values.bindPort }} + protocol: TCP + livenessProbe: + httpGet: + scheme: HTTPS + path: / + port: https + readinessProbe: + httpGet: + scheme: HTTPS + path: / + port: https + volumeMounts: + - name: webhook-certs + mountPath: {{ .Values.certs.mountPath }} + readOnly: true + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: webhook-certs + secret: + secretName: {{ include "node-policy-webhook.fullname" . }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/chart/node-policy-webhook/templates/mutatingwebhook.yaml b/chart/node-policy-webhook/templates/mutatingwebhook.yaml new file mode 100644 index 0000000..3ceac16 --- /dev/null +++ b/chart/node-policy-webhook/templates/mutatingwebhook.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ include "node-policy-webhook.fullname" . }} + labels: + {{- include "node-policy-webhook.labels" . | nindent 4 }} +webhooks: + - name: {{ include "node-policy-webhook.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local + objectSelector: + matchExpressions: + - key: app + operator: NotIn + values: ["node-policy-webhook"] + clientConfig: + caBundle: {{ .Values.caBundle }} + service: + name: node-policy-webhook + namespace: default + path: "/mutate" + port: 443 + rules: + - operations: ["CREATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + sideEffects: None + timeoutSeconds: 5 + reinvocationPolicy: Never + failurePolicy: Ignore diff --git a/chart/node-policy-webhook/templates/nodepolicyprofile_viewer_role.yaml b/chart/node-policy-webhook/templates/nodepolicyprofile_viewer_role.yaml new file mode 100644 index 0000000..f24fd00 --- /dev/null +++ b/chart/node-policy-webhook/templates/nodepolicyprofile_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view nodepolicyprofiles. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nodepolicyprofile-viewer-role +rules: +- apiGroups: + - noodepolicies.softonic.io + resources: + - nodepolicyprofiles + verbs: + - get + - list + - watch +- apiGroups: + - noodepolicies.softonic.io + resources: + - nodepolicyprofiles/status + verbs: + - get diff --git a/chart/node-policy-webhook/templates/noodepolicies.softonic.io_nodepolicyprofiles.yaml b/chart/node-policy-webhook/templates/noodepolicies.softonic.io_nodepolicyprofiles.yaml new file mode 100644 index 0000000..2c84576 --- /dev/null +++ b/chart/node-policy-webhook/templates/noodepolicies.softonic.io_nodepolicyprofiles.yaml @@ -0,0 +1,287 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.5 + creationTimestamp: null + name: nodepolicyprofiles.noodepolicies.softonic.io +spec: + group: noodepolicies.softonic.io + names: + kind: NodePolicyProfile + listKind: NodePolicyProfileList + plural: nodepolicyprofiles + singular: nodepolicyprofile + scope: Cluster + validation: + openAPIV3Schema: + description: NodePolicyProfile is the Schema for the nodepolicyprofiles API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: NodePolicyProfileSpec defines the desired state of NodePolicyProfile + properties: + nodeAffinity: + description: Node affinity is a group of node affinity scheduling rules. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the expressions. + The node that is most preferred is the one with the greatest sum + of weights, i.e. for each node that meets all of the scheduling + requirements (resource request, requiredDuringScheduling affinity + expressions, etc.), compute a sum by iterating through the elements + of this field and adding "weight" to the sum if the node matches + the corresponding matchExpressions; the node(s) with the highest + sum are the most preferred. + items: + description: An empty preferred scheduling term matches all objects + with implicit weight 0 (i.e. it's a no-op). A null preferred + scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the corresponding + weight. + properties: + matchExpressions: + description: A list of node selector requirements by node's + labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: Represents a key's relationship to + a set of values. Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator + is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the + values array must be empty. If the operator is + Gt or Lt, the values array must have a single + element, which will be interpreted as an integer. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements by node's + fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: Represents a key's relationship to + a set of values. Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator + is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the + values array must be empty. If the operator is + Gt or Lt, the values array must have a single + element, which will be interpreted as an integer. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this field + are not met at scheduling time, the pod will not be scheduled + onto the node. If the affinity requirements specified by this + field cease to be met at some point during pod execution (e.g. + due to an update), the system may or may not try to eventually + evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The terms + are ORed. + items: + description: A null or empty node selector term matches no + objects. The requirements of them are ANDed. The TopologySelectorTerm + type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements by node's + labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: Represents a key's relationship to + a set of values. Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator + is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the + values array must be empty. If the operator is + Gt or Lt, the values array must have a single + element, which will be interpreted as an integer. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements by node's + fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: Represents a key's relationship to + a set of values. Valid operators are In, NotIn, + Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the operator + is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the + values array must be empty. If the operator is + Gt or Lt, the values array must have a single + element, which will be interpreted as an integer. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + nodeSelector: + additionalProperties: + type: string + type: object + tolerations: + items: + description: The pod this Toleration is attached to tolerates any + taint that matches the triple using the matching + operator . + properties: + effect: + description: Effect indicates the taint effect to match. Empty + means match all taint effects. When specified, allowed values + are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. If the key is empty, operator + must be Exists; this combination means to match all values and + all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. Exists + is equivalent to wildcard for value, so that a pod can tolerate + all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the + toleration (which must be of effect NoExecute, otherwise this + field is ignored) tolerates the taint. By default, it is not + set, which means tolerate the taint forever (do not evict). + Zero and negative values will be treated as 0 (evict immediately) + by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise + just a regular string. + type: string + type: object + type: array + type: object + status: + description: NodePolicyProfileStatus defines the observed state of NodePolicyProfile + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/chart/node-policy-webhook/templates/secret.yaml b/chart/node-policy-webhook/templates/secret.yaml new file mode 100644 index 0000000..acb7c97 --- /dev/null +++ b/chart/node-policy-webhook/templates/secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "node-policy-webhook.fullname" . }} + labels: + {{- include "node-policy-webhook.labels" . | nindent 4 }} +data: + cert.pem: {{ .Values.certs.cert | b64enc }} + key.pem: {{ .Values.certs.key | b64enc }} +type: Opaque diff --git a/chart/node-policy-webhook/templates/service.yaml b/chart/node-policy-webhook/templates/service.yaml new file mode 100644 index 0000000..7d4981b --- /dev/null +++ b/chart/node-policy-webhook/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "node-policy-webhook.fullname" . }} + labels: + {{- include "node-policy-webhook.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: https + protocol: TCP + name: https + selector: + {{- include "node-policy-webhook.selectorLabels" . | nindent 4 }} diff --git a/chart/node-policy-webhook/templates/serviceaccount.yaml b/chart/node-policy-webhook/templates/serviceaccount.yaml new file mode 100644 index 0000000..a3bd2b6 --- /dev/null +++ b/chart/node-policy-webhook/templates/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "node-policy-webhook.serviceAccountName" . }} + labels: + {{- include "node-policy-webhook.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} diff --git a/chart/node-policy-webhook/values.yaml b/chart/node-policy-webhook/values.yaml new file mode 100644 index 0000000..9bdf114 --- /dev/null +++ b/chart/node-policy-webhook/values.yaml @@ -0,0 +1,67 @@ +# Default values for node-policy-webhook. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: node-policy-webhook + pullPolicy: IfNotPresent + tag: 0.1.0 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +# Secret containing webhook's cert name +certs: + mountPath: /etc/webhook/certs + # Cert cert.pem + cert: "" + # Key key.pem + key: "" + +# Cluster's CA bundle +caBundle: "" + +serviceAccount: + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 443 + +bindPort: 8443 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/cmd/node-policy-webhook/node-policy-webhook.go b/cmd/node-policy-webhook/node-policy-webhook.go new file mode 100644 index 0000000..6174991 --- /dev/null +++ b/cmd/node-policy-webhook/node-policy-webhook.go @@ -0,0 +1,131 @@ +package main + +import ( + "crypto/tls" + "errors" + "fmt" + _ "github.com/golang/glog" + "github.com/softonic/node-policy-webhook/pkg/admission" + h "github.com/softonic/node-policy-webhook/pkg/http" + "github.com/softonic/node-policy-webhook/pkg/version" + "github.com/spf13/cobra" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/klog" + "net/http" + "os" + "path" +) + +type params struct { + version bool + certificate string + privateKey string +} + +const DEFAULT_BIND_ADDRESS = ":8443" + +var handler *h.HttpHandler + +func init() { + klog.V(0).Infof("Starting node-policy-webhook") + + handler = getHttpHandler() +} + +func main() { + var params params + + commandName := path.Base(os.Args[0]) + + rootCmd := &cobra.Command{ + Use: commandName, + Short: fmt.Sprintf("%v handles node policy profiles in kubernetes", commandName), + Run: func(cmd *cobra.Command, args []string) { + if params.version { + fmt.Println("Version:", version.Version) + } else { + run(¶ms) + } + }, + } + rootCmd.Flags().BoolVarP(¶ms.version, "version", "v", false, "print version and exit") + rootCmd.Flags().StringVarP(¶ms.certificate, "tls-cert", "c", "default", "certificate (required)") + rootCmd.Flags().StringVarP(¶ms.privateKey, "tls-key", "p", "default", "privateKey (required)") + + rootCmd.MarkFlagRequired("tls-cert") + rootCmd.MarkFlagRequired("tls-key") + + klog.V(0).Infof("Command initialised") + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(params *params) { + mux := http.NewServeMux() + + _, err := tls.LoadX509KeyPair(params.certificate, params.privateKey) + if err != nil { + klog.Errorf("Failed to load key pair: %v", err) + } + + mux.HandleFunc("/mutate", func(w http.ResponseWriter, r *http.Request) { + handler.MutationHandler(w, r) + }) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + handler.HealthCheckHandler(w, r) + }) + cfg := &tls.Config{ + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + PreferServerCipherSuites: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + } + address := os.Getenv("BIND_ADDRESS") + if address == "" { + address = DEFAULT_BIND_ADDRESS + } + klog.V(0).Infof("Starting server, bund at %v", address) + klog.Infof("Listening to address %v", address) + srv := &http.Server{ + Addr: address, + Handler: mux, + TLSConfig: cfg, + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0), + } + klog.Fatalf("Could not start server: %v", srv.ListenAndServeTLS(params.certificate, params.privateKey)) + +} + +func getHttpHandler() *h.HttpHandler { + return h.NewHttpHanlder(getNodePolicyAdmissionReviewer()) +} + +func getNodePolicyAdmissionReviewer() *admission.AdmissionReviewer { + client, err := getRestClient() + if err != nil { + panic(err.Error()) + } + return admission.NewNodePolicyAdmissionReviewer( + getFetcher(client), + admission.NewPatcher(), + ) +} + +func getFetcher(client dynamic.Interface) admission.FetcherInterface { + return admission.NewNodePolicyProfileFetcher(client) +} +func getRestClient() (dynamic.Interface, error) { + cfg, err := rest.InClusterConfig() + if err != nil { + return nil, errors.New("Error configuring client") + } + return dynamic.NewForConfig(cfg) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ce1931a --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/softonic/node-policy-webhook + +go 1.14 + +require ( + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + github.com/spf13/cobra v1.0.0 + github.com/spf13/pflag v1.0.5 + gotest.tools v2.2.0+incompatible + k8s.io/api v0.18.2 + k8s.io/apimachinery v0.18.5 + k8s.io/client-go v0.18.2 + k8s.io/klog v1.0.0 + sigs.k8s.io/controller-runtime v0.6.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..db1cd48 --- /dev/null +++ b/go.sum @@ -0,0 +1,455 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= +github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.18.2 h1:wG5g5ZmSVgm5B+eHMIbI9EGATS2L8Z72rda19RIEgY8= +k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= +k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY= +k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/apimachinery v0.18.5 h1:Lh6tgsM9FMkC12K5T5QjRm7rDs6aQN5JHkA0JomULDM= +k8s.io/apimachinery v0.18.5/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= +k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw= +k8s.io/client-go v0.18.2 h1:aLB0iaD4nmwh7arT2wIn+lMnAq7OswjaejkQ8p9bBYE= +k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= +k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= +k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmDfUM= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY= +k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= +sigs.k8s.io/controller-runtime v0.6.0 h1:Fzna3DY7c4BIP6KwfSlrfnj20DJ+SeMBK8HSFvOk9NM= +sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..767efde --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/hack/generate_helm_cert_secrets b/hack/generate_helm_cert_secrets new file mode 100755 index 0000000..4758f79 --- /dev/null +++ b/hack/generate_helm_cert_secrets @@ -0,0 +1,101 @@ +#! /bin/bash +set -uo errexit + +export APP="${1}" +export NAMESPACE="${2}" + +export CSR_NAME="${APP}.${NAMESPACE}.svc" + +echo "... creating ${APP}.key" +openssl genrsa -out ssl/${APP}.key 2048 + +echo "... creating ${APP}.csr" +cat >ssl/csr.conf< /dev/null 2>&1 + if [ "$?" -eq 0 ]; then + break + fi + if [[ $SECONDS -ge 60 ]]; then + echo "[!] timed out waiting for csr" + exit 1 + fi + sleep 2 +done + +kubectl certificate approve ${CSR_NAME} + +SECONDS=0 +while true; do + echo "... waiting for serverCert to be present in kubernetes" + echo "kubectl get csr ${CSR_NAME} -o jsonpath='{.status.certificate}'" + serverCert=$(kubectl get csr ${CSR_NAME} -o jsonpath='{.status.certificate}') + if [[ $serverCert != "" ]]; then + break + fi + if [[ $SECONDS -ge 60 ]]; then + echo "[!] timed out waiting for serverCert" + exit 1 + fi + sleep 2 +done + +echo "... creating ${APP}.pem cert file" +echo "\$serverCert | openssl base64 -d -A -out ssl/${APP}.pem" +echo ${serverCert} | openssl base64 -d -A -out ssl/${APP}.pem + +PADDED_CERT=$(cat ssl/${APP}.pem | sed 's/\(.*\)/ \1/') +PADDED_KEY=$(cat ssl/${APP}.key | sed 's/\(.*\)/ \1/') +CA_BUNDLE=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}') + +cat < chart/node-policy-webhook/secret.values.yaml +caBundle: "${CA_BUNDLE}" +certs: + mountPath: /etc/webhook/certs + cert: | +${PADDED_CERT} + key: | +${PADDED_KEY} +EOF diff --git a/pkg/admission/fetcher.go b/pkg/admission/fetcher.go new file mode 100644 index 0000000..524d4e4 --- /dev/null +++ b/pkg/admission/fetcher.go @@ -0,0 +1,42 @@ +package admission + +import ( + "context" + "errors" + "github.com/softonic/node-policy-webhook/api/v1alpha1" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + "k8s.io/klog" +) + +type FetcherInterface interface { + Get(profileName string) (*v1alpha1.NodePolicyProfile, error) +} + +type NodePolicyProfileFetcher struct { + client dynamic.Interface +} + +func NewNodePolicyProfileFetcher(client dynamic.Interface) FetcherInterface { + return &NodePolicyProfileFetcher{ + client: client, + } +} + +func (n *NodePolicyProfileFetcher) Get(profileName string) (*v1alpha1.NodePolicyProfile, error) { + resourceScheme := v1alpha1.SchemeBuilder.GroupVersion.WithResource("nodepolicyprofiles") + + resp, err := n.client.Resource(resourceScheme).Get(context.TODO(), profileName, v12.GetOptions{}) + if err != nil { + klog.Errorf("Error getting NodePolicyProfile %s (%v)", profileName, err) + return nil, errors.New("Error getting NodePolicyProfile") + } + + nodePolicyProfile := &v1alpha1.NodePolicyProfile{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(resp.UnstructuredContent(), nodePolicyProfile) + if err != nil { + return nil, errors.New("NodePolicyProfile not found") + } + return nodePolicyProfile, nil +} diff --git a/pkg/admission/patch.go b/pkg/admission/patch.go new file mode 100644 index 0000000..19eeb2a --- /dev/null +++ b/pkg/admission/patch.go @@ -0,0 +1,105 @@ +package admission + +import ( + "reflect" + "github.com/softonic/node-policy-webhook/api/v1alpha1" + v1 "k8s.io/api/core/v1" +) + +type PatcherInterface interface { + CreatePatch(pod *v1.Pod, nodePolicyProfile *v1alpha1.NodePolicyProfile) *[]PatchOperation +} + +type Patcher struct{} + +type PatchOperation struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +func NewPatcher() PatcherInterface { + return &Patcher{} +} + +func (p *Patcher) CreatePatch(pod *v1.Pod, nodePolicyProfile *v1alpha1.NodePolicyProfile) *[]PatchOperation { + var patch = &[]PatchOperation{} + + p.addNodeSelectorPatch(nodePolicyProfile, patch) + p.addTolerationsPatch(pod, nodePolicyProfile, patch) + p.addNodeAffinityPatch(pod, nodePolicyProfile, patch) + return patch +} + +func (p *Patcher) addNodeAffinityPatch(pod *v1.Pod, nodePolicyProfile *v1alpha1.NodePolicyProfile, patch *[]PatchOperation) { + if reflect.DeepEqual(nodePolicyProfile.Spec.NodeAffinity, v1.NodeAffinity{}) { + return + } + + affinity := v1.Affinity{} + + affinity.NodeAffinity = &nodePolicyProfile.Spec.NodeAffinity + + if pod.Spec.Affinity != nil { + if pod.Spec.Affinity.PodAntiAffinity != nil { + affinity.PodAntiAffinity = pod.Spec.Affinity.PodAntiAffinity + } + + if pod.Spec.Affinity.PodAffinity != nil { + affinity.PodAffinity = pod.Spec.Affinity.PodAffinity + } + } + + *patch = append(*patch, PatchOperation{ + Op: "replace", + Path: "/spec/affinity", + Value: affinity, + }) +} + +func (p *Patcher) addTolerationsPatch(pod *v1.Pod, nodePolicyProfile *v1alpha1.NodePolicyProfile, patch *[]PatchOperation) { + if pod.Spec.Tolerations == nil && nodePolicyProfile.Spec.Tolerations == nil { + return + } + tolerations := []v1.Toleration{} + + tolerations = append(tolerations, pod.Spec.Tolerations...) + + tolerationEqual := false + + for _,tolerationPod := range pod.Spec.Tolerations{ + for _,tolerationProfile := range nodePolicyProfile.Spec.Tolerations{ + if (reflect.DeepEqual(tolerationPod,tolerationProfile )) { + tolerationEqual = true + } + } + } + + if tolerationEqual == false { + tolerations = append(tolerations, nodePolicyProfile.Spec.Tolerations...) + } + + *patch = append(*patch, PatchOperation{ + Op: "replace", + Path: "/spec/tolerations", + Value: tolerations, + }) +} + +func (p *Patcher) addNodeSelectorPatch(nodePolicyProfile *v1alpha1.NodePolicyProfile, patch *[]PatchOperation) { + if nodePolicyProfile.Spec.NodeSelector == nil { + return + } + + nodeSelector := make(map[string]string) + + for key, value := range nodePolicyProfile.Spec.NodeSelector { + nodeSelector[key] = value + } + + *patch = append(*patch, PatchOperation{ + Op: "replace", + Path: "/spec/nodeSelector", + Value: nodeSelector, + }) +} diff --git a/pkg/admission/patch_test.go b/pkg/admission/patch_test.go new file mode 100644 index 0000000..00c19f4 --- /dev/null +++ b/pkg/admission/patch_test.go @@ -0,0 +1,379 @@ +package admission + +import ( + "github.com/softonic/node-policy-webhook/api/v1alpha1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "reflect" + "testing" +) + +var p = &Patcher{} + +func expectPatch(t *testing.T, expectedPatch []PatchOperation, patch *[]PatchOperation) { + if reflect.DeepEqual(expectedPatch, patch) { + t.Errorf("Patch should match expected patch %v, got %v", expectedPatch, patch) + } +} + +func TestCreatePatchWhenNodeSelectorNotSpecified(t *testing.T) { + pod := getPodWithNodeSelector(map[string]string{ + "type": "foobar", + }) + tolerations := &[]v1.Toleration{ + { + Key: "foo", + Operator: "equals", + Value: "bar", + Effect: "NoSchedule", + }, + } + nodePolicyProfile := getNodePolicyProfileWithTolerations(tolerations) + + patch := p.CreatePatch(pod, nodePolicyProfile) + + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/toleration", + Value: tolerations, + }, + } + expectPatch(t, expectedPatch, patch) +} + +func TestCreatePatchWhenNodeSelectorEmpty(t *testing.T) { + nodeSelector := map[string]string{ + "type": "foobar", + } + pod := getPodWithNodeSelector(nodeSelector) + nodePolicyProfile := getNodePolicyProfileWithNodeSelector(map[string]string{}) + + patch := p.CreatePatch(pod, nodePolicyProfile) + + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/nodeSelector", + Value: nodeSelector, + }, + } + expectPatch(t, expectedPatch, patch) +} + +func TestCreatePatchWhenNodeSelectorSameKeyDifferentValue(t *testing.T) { + podNodeSelector := map[string]string{ + "type": "foobar", + } + pod := getPodWithNodeSelector(podNodeSelector) + + profileNodeSelector := map[string]string{ + "type": "barfoo", + } + nodePolicyProfile := getNodePolicyProfileWithNodeSelector(profileNodeSelector) + + patch := p.CreatePatch(pod, nodePolicyProfile) + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/nodeSelector", + Value: profileNodeSelector, + }, + } + expectPatch(t, expectedPatch, patch) +} + +func TestCreatePatchWhenNodeSelectorDifferentKey(t *testing.T) { + podNodeSelector := map[string]string{ + "type": "foobar", + } + pod := getPodWithNodeSelector(podNodeSelector) + + profileNodeSelector := map[string]string{ + "anotherkey": "barfoo", + } + nodePolicyProfile := getNodePolicyProfileWithNodeSelector(profileNodeSelector) + + patch := p.CreatePatch(pod, nodePolicyProfile) + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/nodeSelector", + Value: profileNodeSelector, + }, + } + expectPatch(t, expectedPatch, patch) +} + +func TestCreatePatchWhenNodeAffinity(t *testing.T) { + + + nodeSelectorPod := v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "bar", + Operator: "equals", + Values: []string{ + "foo", + }, + }, + }, + }, + }} + nodeAffinityPod := v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &nodeSelectorPod, + } + + Affinity := v1.Affinity{ + NodeAffinity: &nodeAffinityPod, + } + + pod := getPodWithAffinity(Affinity) + + + nodeSelector := v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: "equals", + Values: []string{ + "bar", + }, + }, + }, + }, + }} + nodeAffinity := v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &nodeSelector, + } + nodePolicyProfile := getNodePolicyProfileWithNodeAffinity(nodeAffinity) + + patch := p.CreatePatch(pod, nodePolicyProfile) + + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/affinity", + Value: nodeAffinity, + }, + } + expectPatch(t, expectedPatch, patch) +} + +func TestCreatePatchPodWithoutNodeAffinity(t *testing.T) { + pod := getPodWithAffinity(v1.Affinity{}) + + nodeSelector := v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: "equals", + Values: []string{ + "bar", + }, + }, + }, + }, + }} + nodeAffinity := v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &nodeSelector, + } + nodePolicyProfile := getNodePolicyProfileWithNodeAffinity(nodeAffinity) + + patch := p.CreatePatch(pod, nodePolicyProfile) + + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/affinity", + Value: nodeAffinity, + }, + } + expectPatch(t, expectedPatch, patch) +} + +func TestCreatePatchTolerationsIdempotent(t *testing.T) { + + tolerations := &[]v1.Toleration{ + { + Key: "foo", + Operator: "equals", + Value: "bar", + Effect: "NoSchedule", + }, + } + + pod := getPodWithTolerations(tolerations) + + nodePolicyProfile := getNodePolicyProfileWithTolerations(tolerations) + + patch := p.CreatePatch(pod, nodePolicyProfile) + + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/tolerations", + Value: tolerations, + }, + } + expectPatch(t, expectedPatch, patch) +} + +func TestCreatePatchWithPodAntiAffinityAndProfileNoAffinity(t *testing.T) { + + WeightedPodAffinityTerm := []v1.WeightedPodAffinityTerm{ + v1.WeightedPodAffinityTerm{ + Weight: 100, + PodAffinityTerm: v1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "foo", + "component": "bar", + "release": "test", + }, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + } + + podAntiAffinity := v1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: WeightedPodAffinityTerm, + } + + + affinity := v1.Affinity{} + + + expectedAffinity := v1.Affinity{ + PodAntiAffinity: &podAntiAffinity, + } + + pod := getPodWithAffinity(affinity) + + nodeAffinity := v1.NodeAffinity{} + + nodePolicyProfile := getNodePolicyProfileWithNodeAffinity(nodeAffinity) + + patch := p.CreatePatch(pod, nodePolicyProfile) + + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/affinity", + Value: expectedAffinity, + }, + } + expectPatch(t, expectedPatch, patch) +} + +func TestCreatePatchWithPodAntiAffinity(t *testing.T) { + pod := getPodWithAffinity(v1.Affinity{}) + + nodeSelector := v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "foo", + Operator: "equals", + Values: []string{ + "bar", + }, + }, + }, + }, + }} + nodeAffinity := v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &nodeSelector, + } + nodePolicyProfile := getNodePolicyProfileWithNodeAffinity(nodeAffinity) + + patch := p.CreatePatch(pod, nodePolicyProfile) + + expectedPatch := []PatchOperation{ + { + Op: "replace", + Path: "/spec/affinity", + Value: nodeAffinity, + }, + } + expectPatch(t, expectedPatch, patch) +} + + +func getNodePolicyProfileWithNodeSelector(nodeSelector map[string]string) *v1alpha1.NodePolicyProfile { + nodePolicyProfile := &v1alpha1.NodePolicyProfile{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1alpha1.NodePolicyProfileSpec{ + NodeSelector: nodeSelector, + }, + Status: v1alpha1.NodePolicyProfileStatus{}, + } + return nodePolicyProfile +} + +func getNodePolicyProfileWithTolerations(tolerations *[]v1.Toleration) *v1alpha1.NodePolicyProfile { + + nodePolicyProfile := &v1alpha1.NodePolicyProfile{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1alpha1.NodePolicyProfileSpec{ + Tolerations: *tolerations, + }, + Status: v1alpha1.NodePolicyProfileStatus{}, + } + return nodePolicyProfile +} + +func getNodePolicyProfileWithNodeAffinity(nodeAffinity v1.NodeAffinity) *v1alpha1.NodePolicyProfile { + return &v1alpha1.NodePolicyProfile{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1alpha1.NodePolicyProfileSpec{ + NodeAffinity: nodeAffinity, + }, + Status: v1alpha1.NodePolicyProfileStatus{}, + } +} + +func getPodWithNodeSelector(nodeSelector map[string]string) *v1.Pod { + pod := &v1.Pod{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1.PodSpec{ + NodeSelector: nodeSelector, + }, + Status: v1.PodStatus{}, + } + return pod +} + +func getPodWithAffinity(affinity v1.Affinity) *v1.Pod { + return &v1.Pod{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1.PodSpec{ + Affinity: &affinity, + }, + Status: v1.PodStatus{}, + } +} + +func getPodWithTolerations(tolerations *[]v1.Toleration) *v1.Pod { + return &v1.Pod{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1.PodSpec{ + Tolerations: *tolerations, + }, + Status: v1.PodStatus{}, + } +} \ No newline at end of file diff --git a/pkg/admission/reviewer.go b/pkg/admission/reviewer.go new file mode 100644 index 0000000..df8a2c4 --- /dev/null +++ b/pkg/admission/reviewer.go @@ -0,0 +1,116 @@ +package admission + +import ( + "encoding/json" + "errors" + "github.com/softonic/node-policy-webhook/pkg/log" + "k8s.io/api/admission/v1beta1" + v1 "k8s.io/api/core/v1" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog" +) + +const PROFILE_ANNOTATION = "nodepolicy.softonic.io/profile" + +type AdmissionReviewer struct { + fetcher FetcherInterface + patcher PatcherInterface +} + +func NewNodePolicyAdmissionReviewer(fetcher FetcherInterface, patcher PatcherInterface) *AdmissionReviewer { + return &AdmissionReviewer{ + fetcher: fetcher, + patcher: patcher, + } +} + +// PerformAdmissionReview : It generates the Adminission Review Response +func (r *AdmissionReviewer) PerformAdmissionReview(admissionReview *v1beta1.AdmissionReview) { + pod, err := r.getPod(admissionReview) + if err != nil { + admissionReview.Response = r.newAdmissionError(pod, err) + return + } + + profile, err := r.getProfile(pod) + if err != nil { + admissionReview.Response = r.admissionAllowedResponse(pod) + return + } + + nodePolicyProfile, err := r.fetcher.Get(profile) + if err != nil { + admissionReview.Response = r.newAdmissionError(pod, err) + return + } + + patch := r.patcher.CreatePatch(pod, nodePolicyProfile) + patchBytes, err := json.Marshal(patch) + if err != nil { + admissionReview.Response = r.newAdmissionError(pod, err) + return + } + + klog.V(log.INFO).Infof("Patching pod %s/%s", pod.Namespace, pod.Name) + patchType := v1beta1.PatchTypeJSONPatch + + admissionReview.Response = &v1beta1.AdmissionResponse{ + Result: &v12.Status{ + Status: "Success", + }, + Patch: patchBytes, + PatchType: &patchType, + Allowed: true, + UID: admissionReview.Request.UID, + } +} + +func (r *AdmissionReviewer) newAdmissionError(pod *v1.Pod, err error) *v1beta1.AdmissionResponse { + if pod != nil { + klog.Errorf("Pod %s/%s failed admission review: %v", pod.Namespace, pod.Name, err) + } else { + klog.Errorf("Failed admission review: %v", err) + } + return &v1beta1.AdmissionResponse{ + Result: &v12.Status{ + Message: err.Error(), + Status: "Fail", + }, + } +} + +func (r *AdmissionReviewer) admissionAllowedResponse(pod *v1.Pod) *v1beta1.AdmissionResponse { + klog.V(log.EXTENDED).Infof("Skipping admission review for pod %s/%s", pod.Namespace, pod.Name) + return &v1beta1.AdmissionResponse{ + Allowed: true, + } +} + +func (r *AdmissionReviewer) getProfile(pod *v1.Pod) (string, error) { + annotations := pod.ObjectMeta.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + + if profileName, ok := annotations[PROFILE_ANNOTATION]; ok { + klog.V(log.INFO).Infof("Successfully found annotation softonic.io/profile. With profile: %v", profileName) + return profileName, nil + } + + return "", errors.New("Annotation not found") +} + +func (r *AdmissionReviewer) getPod(admissionReview *v1beta1.AdmissionReview) (*v1.Pod, error) { + var pod v1.Pod + if admissionReview.Request == nil { + return nil, errors.New("Request is nil") + } + if admissionReview.Request.Object.Raw == nil { + return nil, errors.New("Request object raw is nil") + } + err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod) + if err != nil { + return nil, err + } + return &pod, nil +} diff --git a/pkg/admission/reviewer_test.go b/pkg/admission/reviewer_test.go new file mode 100644 index 0000000..0ce3c85 --- /dev/null +++ b/pkg/admission/reviewer_test.go @@ -0,0 +1,206 @@ +package admission + +import ( + "encoding/json" + "errors" + "github.com/softonic/node-policy-webhook/api/v1alpha1" + "gotest.tools/assert" + "k8s.io/api/admission/v1beta1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "testing" +) + +type MockPatcher struct { + mockPatch *[]PatchOperation + t *testing.T +} + +func (p MockPatcher) CreatePatch(pod *v1.Pod, nodePolicyProfile *v1alpha1.NodePolicyProfile) *[]PatchOperation { + return p.mockPatch +} + +func getMockPatcher(patch *[]PatchOperation, t *testing.T) PatcherInterface { + return &MockPatcher{ + patch, + t, + } +} + +type MockFetcher struct { + mockNodePolicyProfile *v1alpha1.NodePolicyProfile + err error + t *testing.T + expectedProfile string +} + +func (f MockFetcher) Get(name string) (*v1alpha1.NodePolicyProfile, error) { + if f.expectedProfile != "" { + assert.Equal(f.t, f.expectedProfile, name) + } + return f.mockNodePolicyProfile, f.err +} + +func (f MockFetcher) ExpectProfile(expectedProfile string) { + f.expectedProfile = expectedProfile +} + +func getMockFetcher(nodePolicyProfile *v1alpha1.NodePolicyProfile, err error, t *testing.T) FetcherInterface { + return MockFetcher{mockNodePolicyProfile: nodePolicyProfile, err: err, t: t} +} + +func TestFailResponseIfAdmissionReviewRequestEmpty(t *testing.T) { + admissionReview := v1beta1.AdmissionReview{} + + reviewer := NewNodePolicyAdmissionReviewer(getMockFetcher( + &v1alpha1.NodePolicyProfile{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1alpha1.NodePolicyProfileSpec{}, + Status: v1alpha1.NodePolicyProfileStatus{}, + }, nil, t), + getMockPatcher(&[]PatchOperation{ + { + Op: "mock", + Path: "/mock/me", + Value: nil, + }, + }, t), + ) + + reviewer.PerformAdmissionReview(&admissionReview) + + if admissionReview.Response.Result.Status != "Fail" { + t.Errorf("Status should be Fail, but got %v", admissionReview.Response.Result.Status) + } +} + +func TestAllowResponseIfAdmissionReviewRequestPodWithNoAnnotation(t *testing.T) { + pod := v1.Pod{} + rawPod, _ := json.Marshal(pod) + admissionReview := v1beta1.AdmissionReview{ + Request: &v1beta1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: rawPod, + }, + }, + } + + reviewer := NewNodePolicyAdmissionReviewer(getMockFetcher( + &v1alpha1.NodePolicyProfile{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1alpha1.NodePolicyProfileSpec{}, + Status: v1alpha1.NodePolicyProfileStatus{}, + }, nil, t), + getMockPatcher(&[]PatchOperation{ + { + Op: "mock", + Path: "/mock/me", + Value: nil, + }, + }, t), + ) + + reviewer.PerformAdmissionReview(&admissionReview) + if !admissionReview.Response.Allowed { + t.Errorf("Admission review should return true, but got %v", admissionReview.Response.Allowed) + } +} + +func TestModifyResponseIfAdmissionReviewRequestPodWithAnnotation(t *testing.T) { + pod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + PROFILE_ANNOTATION: "testProfile", + }, + }, + } + mockFetcher := getMockFetcher( + &v1alpha1.NodePolicyProfile{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1alpha1.NodePolicyProfileSpec{}, + Status: v1alpha1.NodePolicyProfileStatus{}, + }, nil, t) + + m, _ := mockFetcher.(MockFetcher) + m.ExpectProfile("testProfile") + + rawPod, _ := json.Marshal(pod) + admissionReview := v1beta1.AdmissionReview{ + Request: &v1beta1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: rawPod, + }, + }, + } + patch := &[]PatchOperation{ + { + Op: "mock", + Path: "/mock/me", + Value: nil, + }, + } + jsonPatch, _ := json.Marshal(patch) + reviewer := NewNodePolicyAdmissionReviewer( + mockFetcher, + getMockPatcher(patch, t), + ) + + reviewer.PerformAdmissionReview(&admissionReview) + if !admissionReview.Response.Allowed { + t.Errorf("Admission review should return true, but got %v", admissionReview.Response.Allowed) + } + if admissionReview.Response.Result.Status != "Success" { + t.Errorf("Status should be Success, got %v", admissionReview.Response.Result.Status) + } + assert.DeepEqual(t, admissionReview.Response.Patch, jsonPatch) +} + +func TestFailResponseIfNodePolicyProfileNotFound(t *testing.T) { + pod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + PROFILE_ANNOTATION: "testProfile", + }, + }, + } + mockFetcher := getMockFetcher( + nil, + errors.New("Node Policy Profile not found"), + t, + ) + + m, _ := mockFetcher.(MockFetcher) + m.ExpectProfile("testProfile") + + rawPod, _ := json.Marshal(pod) + admissionReview := v1beta1.AdmissionReview{ + Request: &v1beta1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: rawPod, + }, + }, + } + patch := &[]PatchOperation{ + { + Op: "mock", + Path: "/mock/me", + Value: nil, + }, + } + reviewer := NewNodePolicyAdmissionReviewer( + mockFetcher, + getMockPatcher(patch, t), + ) + + reviewer.PerformAdmissionReview(&admissionReview) + if admissionReview.Response.Allowed { + t.Errorf("Admission review should return false, but got %v", admissionReview.Response.Allowed) + } + if admissionReview.Response.Result.Status != "Fail" { + t.Errorf("Status should be Fail, got %v", admissionReview.Response.Result.Status) + } +} diff --git a/pkg/http/handler.go b/pkg/http/handler.go new file mode 100644 index 0000000..6e150c1 --- /dev/null +++ b/pkg/http/handler.go @@ -0,0 +1,75 @@ +package http + +import ( + "encoding/json" + "errors" + "github.com/softonic/node-policy-webhook/pkg/admission" + "io" + "k8s.io/api/admission/v1beta1" + "k8s.io/klog" + "net/http" +) + +type HttpHandler struct { + reviewer *admission.AdmissionReviewer +} + +func NewHttpHanlder(reviewer *admission.AdmissionReviewer) *HttpHandler { + return &HttpHandler{ + reviewer: reviewer, + } +} + +func (h *HttpHandler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) { + klog.Warning("Responding HC") + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte("ok")); err != nil { + failIfError(w, err) + } +} + +func (h *HttpHandler) MutationHandler(w http.ResponseWriter, r *http.Request) { + + if err, status := validateRequest(r); err != nil { + http.Error(w, err.Error(), status) + return + } + + resp, err := h.getResponse(r.Body) + if err != nil { + failIfError(w, err) + } + + if _, err := w.Write(resp); err != nil { + failIfError(w, err) + } + +} + +func (h *HttpHandler) getResponse(rawAdmissionReview io.Reader) ([]byte, error) { + admissionReview := &v1beta1.AdmissionReview{} + err := json.NewDecoder(rawAdmissionReview).Decode(admissionReview) + if err != nil { + return nil, err + } + + h.reviewer.PerformAdmissionReview(admissionReview) + + resp, err := json.Marshal(admissionReview) + return resp, err +} + +func failIfError(w http.ResponseWriter, err error) { + if err != nil { + klog.Errorf("Can't write response: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + +func validateRequest(r *http.Request) (error, int) { + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + return errors.New("invalid Content-Type, expect `application/json`"), http.StatusUnsupportedMediaType + } + return nil, 0 +} diff --git a/pkg/log/level.go b/pkg/log/level.go new file mode 100644 index 0000000..cb72175 --- /dev/null +++ b/pkg/log/level.go @@ -0,0 +1,10 @@ +package log + +// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md + +const WARNING = 0 +const NOTICE = 1 +const INFO = 2 +const EXTENDED = 3 +const DEBUG = 4 +const TRACE = 5 diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000..83dae14 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,5 @@ +package version + +var ( + Version = "" +) diff --git a/samples/nodepolicyprofile.yaml b/samples/nodepolicyprofile.yaml new file mode 100644 index 0000000..0bce854 --- /dev/null +++ b/samples/nodepolicyprofile.yaml @@ -0,0 +1,21 @@ +apiVersion: noodepolicies.softonic.io/v1alpha1 +kind: NodePolicyProfile +metadata: + name: test +spec: + nodeSelector: + type: "stateless" + tolerations: + - key: "type" + operator: "Equal" + value: "stateless" + effect: "NoSchedule" + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - preference: + matchExpressions: + - key: type + operator: In + values: + - stateless + weight: 1 diff --git a/ssl/csr.conf b/ssl/csr.conf new file mode 100644 index 0000000..99f8ed5 --- /dev/null +++ b/ssl/csr.conf @@ -0,0 +1,14 @@ +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name +[req_distinguished_name] +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names +[alt_names] +DNS.1 = node-policy-webhook +DNS.2 = node-policy-webhook.default +DNS.3 = node-policy-webhook.default.svc +DNS.4 = node-policy-webhook.default.svc.cluster.local