commit b12356ce24164af3d64fcc476e6126f32e98fd63 Author: Jakub Vavřík Date: Mon Jun 28 14:42:47 2021 +0200 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2fc6308 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +vendor/* + +build/_output + +.go +.idea \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb73ef6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +vendor/* + +build/_output + +.go +.idea + +TODO.md \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d51cea8 --- /dev/null +++ b/Makefile @@ -0,0 +1,113 @@ +# Image URL to use all building/pushing image targets; +# Use your own docker registry and image name for dev/test by overridding the +# IMAGE_REPO, IMAGE_NAME and IMAGE_TAG environment variable. +REPOSITORY_BASE ?= ghcr.io +IMAGE_REPO ?= $(REPOSITORY_BASE)/ysoftdevs/secret-duplicator +IMAGE_NAME ?= secret-duplicator +GENERATOR_IMAGE_NAME ?= webhook-cert-generator + +# Github host to use for checking the source tree; +GIT_HOST ?= github.com/ysoftdevs + +PWD := $(shell pwd) +BASE_DIR := $(shell basename $(PWD)) +REPO_ROOT := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +# Keep an existing GOPATH, make a private one if it is undefined +GOPATH_DEFAULT := $(PWD)/.go +export GOPATH ?= $(GOPATH_DEFAULT) +TESTARGS_DEFAULT := "-v" +export TESTARGS ?= $(TESTARGS_DEFAULT) +DEST := $(GOPATH)/src/$(GIT_HOST)/$(BASE_DIR) +IMAGE_TAG ?= $(shell cat "$(REPO_ROOT)/VERSION") + + +LOCAL_OS := $(shell uname) +ifeq ($(LOCAL_OS),Linux) + TARGET_OS ?= linux + XARGS_FLAGS="-r" +else ifeq ($(LOCAL_OS),Darwin) + TARGET_OS ?= darwin + XARGS_FLAGS= +else + $(error "This system's OS $(LOCAL_OS) isn't recognized/supported") +endif + +all: fmt lint test build image + +ifeq (,$(wildcard go.mod)) +ifneq ("$(realpath $(DEST))", "$(realpath $(PWD))") + $(error Please run 'make' from $(DEST). Current directory is $(PWD)) +endif +endif + +############################################################ +# format section +############################################################ + +fmt: + @go fmt ./cmd/... + +############################################################ +# lint section +############################################################ + +lint: + @echo "Runing the golangci-lint..." + +############################################################ +# test section +############################################################ + +test: + @echo "Running the tests for $(IMAGE_NAME)..." + @go test $(TESTARGS) ./... + +############################################################ +# build section +############################################################ + +build: + @echo "Building the $(IMAGE_NAME) binary..." + @CGO_ENABLED=0 go build -o build/_output/bin/$(IMAGE_NAME) ./cmd/ + +build-linux: + @echo "Building the $(IMAGE_NAME) binary for Docker (linux)..." + @GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o build/_output/linux/bin/$(IMAGE_NAME) ./cmd/ + +############################################################ +# image section +############################################################ + +image: docker-login build-image push-image + +docker-login: + @echo "$(DOCKER_TOKEN)" | docker login -u "$(DOCKER_USER)" --password-stdin "$(REPOSITORY_BASE)" + +docker-logout: + @docker logout + +build-image: + @echo "Building the docker image: $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG)..." + @docker build -t $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG) -f build/Dockerfile . + @echo "Building the docker image: $(IMAGE_REPO)/$(GENERATOR_IMAGE_NAME):$(IMAGE_TAG)..." + @docker build -t $(IMAGE_REPO)/$(GENERATOR_IMAGE_NAME):$(IMAGE_TAG) -f build/Dockerfile.cert-generator . + +push-image: build-image + @echo "Pushing the docker image for $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG) and $(IMAGE_REPO)/$(IMAGE_NAME):latest..." + @docker tag $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_REPO)/$(IMAGE_NAME):latest + @docker push $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG) + @docker push $(IMAGE_REPO)/$(IMAGE_NAME):latest + @echo "Pushing the docker image for $(IMAGE_REPO)/$(GENERATOR_IMAGE_NAME):$(IMAGE_TAG) and $(IMAGE_REPO)/$(GENERATOR_IMAGE_NAME):latest..." + @docker tag $(IMAGE_REPO)/$(GENERATOR_IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_REPO)/$(GENERATOR_IMAGE_NAME):latest + @docker push $(IMAGE_REPO)/$(GENERATOR_IMAGE_NAME):$(IMAGE_TAG) + @docker push $(IMAGE_REPO)/$(GENERATOR_IMAGE_NAME):latest + + +############################################################ +# clean section +############################################################ +clean: + @rm -rf build/_output + +.PHONY: all fmt lint check test build image clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..d456e38 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Kubernetes Mutating Webhook for ImagePullSecret injection in ServiceAccounts + +The responsibility of this webhook is to patch all newly created/updated service account and make sure they all contained proper imagepullsecret configuration. + +This repo produces one helm chart available via helm repository https://ysoftdevs.github.io/imagepullsecret-injector. There are also 2 docker images: +- `ghcr.io/ysoftdevs/imagepullsecret-injector/imagepullsecret-injector` - the image containing the webhook itself +- `ghcr.io/ysoftdevs/imagepullsecret-injector/webhook-cert-generator` - helper image responsible for (re)generating the certificates + +## Helm description +The helm chart consists of 2 parts: the certificate generator and the webhook configuration itself. + +Certificate generation part periodically generates certificates signed by kubernetes' CA and passes them to the webhook where they are used as server-side certificates. The flow works roughly like this: +1. We generate a CSR using openssl and tie the certificate to the webhook's service DNS. +1. We create a k8s CertificateSigningRequest from the openssl CSR. +1. We approve this request using our special ServiceAccount with approve permissions. This makes kubernetes issue the certificate +1. We fetch the certificate from the k8s CSR (at `.status.certificate`) and create a secret from it +1. We also create a CronJob that does this periodically as k8s only issues certificates for 1 year + +The main part is the deployment and the web hook configuration. The flow is as follows +1. The MutatingWebhookConfiguration we create instructs k8s to pass all requests for creating/updating all ServiceAccounts to our webhook before finishing the request +1. We check whether the SA has the correctly defined imagepullsecret configuration. if not, we create a patch for the resource +1. We also check whether we have the secret we are using in the imagepullsecret in the SA's namespace. If not, we create it based on our source secret +1. We return the patch to k8s, which applies the changes + +Of note is also a fact that the chart runs a lookup to the connected cluster to fetch the CA bundle for the MutatingWebhook. This means `helm template` won't work. + +## Running locally +1. Create the prerequisite resources: + ```bash + kubectl create ns imagepullsecret-injector + + kubectl create secret -n imagepullsecret-injector \ + generic acr-dockerconfigjson-source \ + --type=kubernetes.io/dockerconfigjson \ + --from-literal=.dockerconfigjson='' + ``` + +1. Build the images and run the chart + ``` bash + make build-image + helm upgrade -i imagepullsecret-injector \ + -n imagepullsecret-injector \ + charts/imagepullsecret-injector + ``` + Alternatively, you can use the pre-built, publicly available helm chart and docker images: + ```bash + helm repo add imagepullsecret-injector https://ysoftdevs.github.io/imagepullsecret-injector + helm repo update + helm upgrade -i imagepullsecret-injector \ + -n imagepullsecret-injector \ + magepullsecret-injector/imagepullsecret-injector + ``` + +1. To test whether everything works, you can run + ```bash + kubectl create ns yolo + kubectl get sa -n yolo default -ojsonpath='{.imagePullSecrets}' + ``` + The `get` command should display _some_ non-empty result. + +## Releasing locally +To authenticate to the docker registry to push the images manually, you will need your own Github Personal Access Token. For more information follow this guide https://docs.github.com/en/packages/guides/migrating-to-github-container-registry-for-docker-images#authenticating-with-the-container-registry \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..8a9ecc2 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1 \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..29a788f --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.15 AS builder + +WORKDIR /go/src/github.com/ysoftdevs/imagepullsecret-injector +COPY . . +RUN make build + +FROM alpine:3.13.4 as base +COPY --from=builder /go/src/github.com/ysoftdevs/imagepullsecret-injector/build/_output/bin/imagepullsecret-injector /usr/local/bin/imagepullsecret-injector + +ENTRYPOINT ["imagepullsecret-injector"] \ No newline at end of file diff --git a/build/Dockerfile.cert-generator b/build/Dockerfile.cert-generator new file mode 100644 index 0000000..93cbb7c --- /dev/null +++ b/build/Dockerfile.cert-generator @@ -0,0 +1,6 @@ +FROM alpine:3.13.4 + +RUN apk add bash curl openssl \ + && curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + && chmod 755 ./kubectl \ + && mv ./kubectl /usr/bin/kubectl diff --git a/charts/secret-duplicator/.helmignore b/charts/secret-duplicator/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/secret-duplicator/.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/charts/secret-duplicator/Chart.yaml b/charts/secret-duplicator/Chart.yaml new file mode 100644 index 0000000..fd26cfe --- /dev/null +++ b/charts/secret-duplicator/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: secret-duplicator +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.0.1 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 0.0.1 diff --git a/charts/secret-duplicator/scripts/create-signed-cert.sh b/charts/secret-duplicator/scripts/create-signed-cert.sh new file mode 100644 index 0000000..2e461d7 --- /dev/null +++ b/charts/secret-duplicator/scripts/create-signed-cert.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +set -e + +usage() { + cat <> "${tmpdir}"/csr.conf +[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 = ${service} +DNS.2 = ${service}.${namespace} +DNS.3 = ${service}.${namespace}.svc +EOF + +openssl genrsa -out "${tmpdir}"/server-key.pem 2048 +openssl req -new -key "${tmpdir}"/server-key.pem -subj "/O=system:nodes/CN=system:node:${service}.${namespace}.svc" -out "${tmpdir}"/server.csr -config "${tmpdir}"/csr.conf + +# clean-up any previously created CSR for our service. Ignore errors if not present. +echo "Deleting old CertificateSigningRequests" +kubectl delete csr ${csrName} 2>/dev/null || true + +echo "Creating new CertificateSigningRequests" +# create server cert/key CSR and send to k8s API +jq -n --arg request "$(< "${tmpdir}"/server.csr base64 -w0)" \ + --arg namespace "$namespace" \ + --arg csrName "$csrName" '{ + apiVersion: "certificates.k8s.io/v1beta1", + kind: "CertificateSigningRequest", + metadata: { + name: $csrName, + namespace: $namespace + }, + spec: { + signerName: "kubernetes.io/kubelet-serving", + groups: ["system:authenticated"], + request: $request, + usages: [ + "digital signature", + "key encipherment", + "server auth" + ] + } + }' | kubectl create -f - + +# verify CSR has been created +while true; do + if kubectl get csr ${csrName}; then + break + else + sleep 1 + fi +done + +echo "Approving CertificateSigningRequests" +# approve and fetch the signed certificate +kubectl certificate approve ${csrName} + +echo "Fetching certificate from approved CertificateSigningRequests" +# verify certificate has been signed +for _ in $(seq 10); do + serverCert=$(kubectl get csr ${csrName} -o jsonpath='{.status.certificate}') + if [[ ${serverCert} != '' ]]; then + break + fi + sleep 1 +done +if [[ ${serverCert} == '' ]]; then + echo "ERROR: After approving csr ${csrName}, the signed certificate did not appear on the resource. Giving up after 10 attempts." >&2 + exit 1 +fi +echo "${serverCert}" | openssl base64 -d -A -out "${tmpdir}"/server-cert.pem + +echo "Creating secret $secret based on the retrieved certificate" +# create the secret with CA cert and server cert/key +kubectl create secret generic ${secret} \ + --from-file=key.pem="${tmpdir}"/server-key.pem \ + --from-file=cert.pem="${tmpdir}"/server-cert.pem \ + --dry-run=client -o yaml | + kubectl -n ${namespace} apply -f - diff --git a/charts/secret-duplicator/templates/_helpers.tpl b/charts/secret-duplicator/templates/_helpers.tpl new file mode 100644 index 0000000..41d43f1 --- /dev/null +++ b/charts/secret-duplicator/templates/_helpers.tpl @@ -0,0 +1,81 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "secret-duplicator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "secret-duplicator.serviceName" -}} +ips-injector-svc +{{- end }} + +{{- define "secret-duplicator.certificateSecretName" -}} +{{ include "secret-duplicator.name" . }}-webhook-certs +{{- end }} + +{{- define "secret-duplicator.lookupCaBundle" -}} +{{- /* Find the name of the secret corresponding to the default SA in the default namespace */ -}} +{{- /* Equivalent to `kubectl get sa -n default default -ojsonpath='{.secrets[0].name}'` */ -}} +{{- $defaultSecretName := ((lookup "v1" "ServiceAccount" "default" "default").secrets | first).name -}} +{{- /* Fetch the ca.crt from the default secret (still base64-encoded)*/ -}} +{{- /* Equivalent to `kubectl get secret -n default $defaultSecretName -ojsonpath='{.data.ca\.crt}'` */ -}} +{{- $caBundle := get (lookup "v1" "Secret" "default" $defaultSecretName ).data "ca.crt" -}} +{{- $caBundle -}} +{{- 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 "secret-duplicator.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 "secret-duplicator.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "secret-duplicator.labels" -}} +helm.sh/chart: {{ include "secret-duplicator.chart" . }} +{{ include "secret-duplicator.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "secret-duplicator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "secret-duplicator.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "secret-duplicator.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "secret-duplicator.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/secret-duplicator/templates/certificate-gen/configmap-certigicate-gen.yaml b/charts/secret-duplicator/templates/certificate-gen/configmap-certigicate-gen.yaml new file mode 100644 index 0000000..a7b3387 --- /dev/null +++ b/charts/secret-duplicator/templates/certificate-gen/configmap-certigicate-gen.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: secret-duplicator-cert-gen-entrypoint + namespace: {{ .Release.Namespace }} + labels: + {{- include "secret-duplicator.labels" . | nindent 4 }} +data: + entrypoint.sh: | {{ .Files.Get "scripts/create-signed-cert.sh" | nindent 4 }} diff --git a/charts/secret-duplicator/templates/certificate-gen/cronjob-certificate-gen.yaml b/charts/secret-duplicator/templates/certificate-gen/cronjob-certificate-gen.yaml new file mode 100644 index 0000000..aa95c0c --- /dev/null +++ b/charts/secret-duplicator/templates/certificate-gen/cronjob-certificate-gen.yaml @@ -0,0 +1,41 @@ +apiVersion: batch/v1beta1 +kind: CronJob +metadata: + name: "{{ .Release.Name }}-cert-gen-cron-job" + labels: + {{- include "secret-duplicator.labels" . | nindent 4 }} +spec: + schedule: '* * * * 0' + jobTemplate: + metadata: + name: "{{ .Release.Name }}" + labels: + {{- include "secret-duplicator.labels" . | nindent 8 }} + spec: + ttlSecondsAfterFinished: 30 + template: + spec: + serviceAccountName: secret-duplicator-cert-gen + restartPolicy: Never + containers: + - name: pre-install-job + image: "{{ .Values.certificateGeneratorImage.registry }}/{{ .Values.certificateGeneratorImage.repository }}:{{ .Values.certificateGeneratorImage.tag | default .Chart.AppVersion }}" + command: ["/entrypoint/entrypoint.sh"] + args: + - --service + - "{{ include "secret-duplicator.serviceName" . }}" + - --namespace + - "{{ .Release.Namespace }}" + - --secret + - "{{ include "secret-duplicator.certificateSecretName" . }}" + volumeMounts: + - mountPath: "/entrypoint" + name: entrypoint + volumes: + - name: entrypoint + configMap: + name: secret-duplicator-cert-gen-entrypoint + items: + - key: entrypoint.sh + path: entrypoint.sh + mode: 0755 diff --git a/charts/secret-duplicator/templates/certificate-gen/job-certificate-gen.yaml b/charts/secret-duplicator/templates/certificate-gen/job-certificate-gen.yaml new file mode 100644 index 0000000..624422c --- /dev/null +++ b/charts/secret-duplicator/templates/certificate-gen/job-certificate-gen.yaml @@ -0,0 +1,34 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ .Release.Name }}-cert-gen-job" + labels: + {{- include "secret-duplicator.labels" . | nindent 4 }} +spec: + ttlSecondsAfterFinished: 30 + template: + spec: + serviceAccountName: secret-duplicator-cert-gen + restartPolicy: Never + containers: + - name: pre-install-job + image: "{{ .Values.certificateGeneratorImage.registry }}/{{ .Values.certificateGeneratorImage.repository }}:{{ .Values.certificateGeneratorImage.tag | default .Chart.AppVersion }}" + command: ["/entrypoint/entrypoint.sh"] + args: + - --service + - "{{ include "secret-duplicator.serviceName" . }}" + - --namespace + - "{{ .Release.Namespace }}" + - --secret + - "{{ include "secret-duplicator.certificateSecretName" . }}" + volumeMounts: + - mountPath: "/entrypoint" + name: entrypoint + volumes: + - name: entrypoint + configMap: + name: secret-duplicator-cert-gen-entrypoint + items: + - key: entrypoint.sh + path: entrypoint.sh + mode: 0755 diff --git a/charts/secret-duplicator/templates/certificate-gen/rbac-certificate-gen.yaml b/charts/secret-duplicator/templates/certificate-gen/rbac-certificate-gen.yaml new file mode 100644 index 0000000..025b298 --- /dev/null +++ b/charts/secret-duplicator/templates/certificate-gen/rbac-certificate-gen.yaml @@ -0,0 +1,69 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: secret-duplicator-cert-gen + namespace : {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + k8s-app: secret-duplicator-cert-gen + name: secret-duplicator-cert-gen +rules: + - apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - list + - patch + - create + - get + - delete + - apiGroups: + - "" + resources: + - namespaces + verbs: + - list + - get + - apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests + verbs: + - create + - list + - get + - delete + - apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests/approval + verbs: + - update + - apiGroups: + - certificates.k8s.io + resources: + - signers + resourceNames: + - kubernetes.io/kubelet-serving + verbs: + - approve +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: secret-duplicator-cert-gen + labels: + {{- include "secret-duplicator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: secret-duplicator-cert-gen +subjects: + - kind: ServiceAccount + name: secret-duplicator-cert-gen + namespace : {{ .Release.Namespace }} diff --git a/charts/secret-duplicator/templates/cm.yaml b/charts/secret-duplicator/templates/cm.yaml new file mode 100644 index 0000000..fba5a05 --- /dev/null +++ b/charts/secret-duplicator/templates/cm.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: tmp + namespace: {{ .Release.Namespace }} +data: + caBundle: {{ include "secret-duplicator.lookupCaBundle" . | quote }} diff --git a/charts/secret-duplicator/templates/deployment.yaml b/charts/secret-duplicator/templates/deployment.yaml new file mode 100644 index 0000000..5f35758 --- /dev/null +++ b/charts/secret-duplicator/templates/deployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: secret-duplicator-webhook-deployment + namespace : {{ .Release.Namespace }} + labels: + app: secret-duplicator + {{- include "secret-duplicator.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app: secret-duplicator + template: + metadata: + labels: + app: secret-duplicator + spec: + serviceAccountName: secret-duplicator + containers: + - name: secret-duplicator + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: IfNotPresent + args: + - -alsologtostderr + - -v=4 + - 2>&1 + env: + - name: CONFIG_PORT + value: "8443" + - name: CONFIG_CERT_PATH + value: "/etc/webhook/certs/cert.pem" + - name: CONFIG_KEY_PATH + value: "/etc/webhook/certs/key.pem" + - name: CONFIG_EXCLUDE_NAMESPACES + value: {{ join "," .Values.secretDuplicator.excludeNamespaces | quote }} + - name: CONFIG_TARGET_SECRET_NAME + value: {{ .Values.secretDuplicator.targetSecretName | quote }} + - name: CONFIG_TARGET_SECRET_ANNOTATION + value: {{ .Values.secretDuplicator.targetSecretAnnotation | quote }} + volumeMounts: + - name: webhook-certs + mountPath: /etc/webhook/certs + readOnly: true + volumes: + - name: webhook-certs + secret: + secretName: {{ include "secret-duplicator.certificateSecretName" . }} \ No newline at end of file diff --git a/charts/secret-duplicator/templates/mutatingwebhook.yaml b/charts/secret-duplicator/templates/mutatingwebhook.yaml new file mode 100644 index 0000000..327854e --- /dev/null +++ b/charts/secret-duplicator/templates/mutatingwebhook.yaml @@ -0,0 +1,25 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: secret-duplicator-webhook-cfg + namespace: {{ .Release.Namespace }} + labels: + app: secret-duplicator + {{- include "secret-duplicator.labels" . | nindent 4 }} +webhooks: +- name: secret-duplicator.ysoftdevs.github.com + clientConfig: + service: + name: {{ include "secret-duplicator.serviceName" . }} + namespace : {{ .Release.Namespace }} + path: "/mutate" + caBundle: {{ include "secret-duplicator.lookupCaBundle" . }} + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["serviceaccounts"] + admissionReviewVersions: ["v1", "v1beta1"] + sideEffects: None + # The default "Fail" option prevents Gardener cluster to be hibernated + failurePolicy: Ignore diff --git a/charts/secret-duplicator/templates/rbac.yaml b/charts/secret-duplicator/templates/rbac.yaml new file mode 100644 index 0000000..c7f3b18 --- /dev/null +++ b/charts/secret-duplicator/templates/rbac.yaml @@ -0,0 +1,72 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: secret-duplicator + namespace : {{ .Release.Namespace }} + labels: + {{- include "secret-duplicator.labels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + k8s-app: secret-duplicator + {{- include "secret-duplicator.labels" . | nindent 4 }} + name: secret-duplicator +rules: + - apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - list + - patch + - create + - get + - delete + - update + - apiGroups: + - "" + resources: + - namespaces + verbs: + - list + - get + - apiGroups: + - "certificates.k8s.io/v1" + resources: + - certificatesigningrequests + verbs: + - create + - list + - get + - apiGroups: + - "certificates.k8s.io/v1" + resources: + - certificatesigningrequests/approval + verbs: + - update + - apiGroups: + - certificates.k8s.io + resources: + - signers + resourceNames: + - kubernetes.io/kubelet-serving + verbs: + - approve +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: secret-duplicator + labels: + {{- include "secret-duplicator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: secret-duplicator +subjects: + - kind: ServiceAccount + name: secret-duplicator + namespace : {{ .Release.Namespace }} diff --git a/charts/secret-duplicator/templates/service.yaml b/charts/secret-duplicator/templates/service.yaml new file mode 100644 index 0000000..495c7b4 --- /dev/null +++ b/charts/secret-duplicator/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "secret-duplicator.serviceName" . }} + namespace : {{ .Release.Namespace }} + labels: + app: secret-duplicator + {{- include "secret-duplicator.labels" . | nindent 4 }} +spec: + ports: + - port: 443 + targetPort: 8443 + selector: {{ include "secret-duplicator.selectorLabels" . | nindent 4 }} diff --git a/charts/secret-duplicator/values.yaml b/charts/secret-duplicator/values.yaml new file mode 100644 index 0000000..eebd802 --- /dev/null +++ b/charts/secret-duplicator/values.yaml @@ -0,0 +1,19 @@ +image: + registry: ghcr.io/ysoftdevs/secret-duplicator + repository: secret-duplicator + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +certificateGeneratorImage: + registry: ghcr.io/ysoftdevs/secret-duplicator + repository: webhook-cert-generator + tag: "" + +secretDuplicator: + targetSecretName: "dashboard-terminal-kube-apiserver-tls" + targetSecretAnnotation: "reflector.v1.k8s.emberstack.com/reflects=cert-manager/default-cert" + excludeNamespaces: + - kube-system + - traefik + - datadog diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..ad0425a --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "crypto/tls" + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/golang/glog" +) + +func main() { + parameters := DefaultParametersObject() + + // get command line parameters + flag.IntVar(¶meters.port, "port", LookupIntEnv("CONFIG_PORT", parameters.port), "Webhook server port.") + flag.StringVar(¶meters.certFile, "tlsCertFile", LookupStringEnv("CONFIG_CERT_PATH", parameters.certFile), "File containing the x509 Certificate for HTTPS.") + flag.StringVar(¶meters.keyFile, "tlsKeyFile", LookupStringEnv("CONFIG_KEY_PATH", parameters.keyFile), "File containing the x509 private key to --tlsCertFile.") + flag.StringVar(¶meters.excludeNamespaces, "excludeNamespaces", LookupStringEnv("CONFIG_EXCLUDE_NAMESPACES", parameters.excludeNamespaces), "Comma-separated namespace names to ignore.") + + flag.StringVar(¶meters.targetSecretAnnotation, "targetSecretAnnotation", LookupStringEnv("CONFIG_TARGET_SECRET_ANNOTATION", parameters.targetSecretAnnotation), "Annotation of the targetSecret secret we will create in the namespace") + flag.StringVar(¶meters.targetSecretName, "targetSecretName", LookupStringEnv("CONFIG_TARGET_SECRET_NAME", parameters.targetSecretName), "Name of the targetSecret secret we will create in the namespace") + + flag.Parse() + + glog.Infof("Running with config: %+v", parameters) + + whsvr, err := NewWebhookServer( + ¶meters, + &http.Server{ + Addr: fmt.Sprintf(":%v", parameters.port), + // This is quite inefficient as it loads file contents on every TLS ClientHello, but ¯\_(ツ)_/¯ + TLSConfig: &tls.Config{GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + glog.Infof("Loading certificates") + cert, err := tls.LoadX509KeyPair(parameters.certFile, parameters.keyFile) + return &cert, err + }}, + }, + ) + if err != nil { + glog.Exitf("Could not create the Webhook server: %v", err) + } + + // define http server and server handler + mux := http.NewServeMux() + mux.HandleFunc("/mutate", whsvr.serve) + whsvr.server.Handler = mux + + // start webhook server in new rountine + go func() { + if err := whsvr.server.ListenAndServeTLS(parameters.certFile, parameters.keyFile); err != nil { + glog.Errorf("Failed to listen and serve webhook server: %v", err) + } + }() + + // listening OS shutdown singal + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + <-signalChan + + glog.Infof("Got OS shutdown signal, shutting down webhook server gracefully...") + if err := whsvr.server.Shutdown(context.Background()); err != nil { + glog.Errorf("Error while shutting down: %v", err) + } +} diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..2aa9516 --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,55 @@ +package main + +import ( + "io/ioutil" + "os" + "strconv" + "strings" +) + +// LookupStringEnv either returns the the value of the env variable, or the provided default value, if the env doesn't exist +func LookupStringEnv(envName string, defVal string) string { + if envVal, exists := os.LookupEnv(envName); exists { + return envVal + } + + return defVal +} + +// LookupBoolEnv either returns the the value of the env variable, or the provided default value, if the env doesn't exist +func LookupBoolEnv(envName string, defVal bool) bool { + if envVal, exists := os.LookupEnv(envName); exists { + if boolVal, err := strconv.ParseBool(envVal); err == nil { + return boolVal + } + } + + return defVal +} + +// LookupIntEnv either returns the the value of the env variable, or the provided default value, if the env doesn't exist +func LookupIntEnv(envName string, defVal int) int { + if envVal, exists := os.LookupEnv(envName); exists { + if intVal, err := strconv.Atoi(envVal); err == nil { + return intVal + } + } + + return defVal +} + +func getCurrentNamespace() string { + // Check whether we have overridden the namespace + if ns, ok := os.LookupEnv("POD_NAMESPACE"); ok { + return ns + } + + // Fall back to the namespace associated with the service account token, if available (this should exist if running in a K8S pod) + if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { + if ns := strings.TrimSpace(string(data)); len(ns) > 0 { + return ns + } + } + + return "default" +} diff --git a/cmd/webhook.go b/cmd/webhook.go new file mode 100644 index 0000000..1568cc0 --- /dev/null +++ b/cmd/webhook.go @@ -0,0 +1,317 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "k8s.io/apimachinery/pkg/api/errors" + "net/http" + "strings" + + "github.com/golang/glog" + "k8s.io/api/admission/v1beta1" + admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" + "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +var ( + runtimeScheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(runtimeScheme) + deserializer = codecs.UniversalDeserializer() +) + +type WebhookServer struct { + server *http.Server + config *WhSvrParameters + client *kubernetes.Clientset +} + +// WhSvrParameters represents all configuration options available though cmd parameters or env variables +type WhSvrParameters struct { + port int + certFile string + keyFile string + excludeNamespaces string + targetSecretName string + targetSecretAnnotation string +} + +var ( + defaultIgnoredNamespaces = []string{ + metav1.NamespaceSystem, + metav1.NamespacePublic, + } + + defaultServiceAccounts = []string{ + "default", + } +) + +// NewWebhookServer constructor for WebhookServer +func NewWebhookServer(parameters *WhSvrParameters, server *http.Server) (*WebhookServer, error) { + config, err := rest.InClusterConfig() + if err != nil { + glog.Errorf("Could not create k8s client: %v", err) + return nil, err + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + glog.Errorf("Could not create k8s clientset: %v", err) + return nil, err + } + + return &WebhookServer{ + config: parameters, + server: server, + client: clientset, + }, nil + +} + +// DefaultParametersObject returns a parameters object with the default values +func DefaultParametersObject() WhSvrParameters { + return WhSvrParameters{ + port: 8443, + certFile: "/etc/webhook/certs/cert.pem", + keyFile: "/etc/webhook/certs/key.pem", + excludeNamespaces: strings.Join(defaultIgnoredNamespaces, ","), + targetSecretName: "dashboard-terminal-kube-apiserver-tls", + targetSecretAnnotation: "reflector.v1.k8s.emberstack.com/reflects=cert-manager/default-cert", + } +} + +type patchOperation struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +func init() { + _ = corev1.AddToScheme(runtimeScheme) + _ = admissionregistrationv1beta1.AddToScheme(runtimeScheme) + // defaulting with webhooks: + // https://github.com/kubernetes/kubernetes/issues/57982 + _ = v1.AddToScheme(runtimeScheme) +} + +func addImagePullSecret(target, added []corev1.LocalObjectReference, basePath string) (patch []patchOperation) { + first := len(target) == 0 + var value interface{} + for _, add := range added { + value = add + path := basePath + if first { + first = false + value = []corev1.LocalObjectReference{add} + } else { + path = path + "/-" + } + patch = append(patch, patchOperation{ + Op: "add", + Path: path, + Value: value, + }) + } + return patch +} + +// ensureSecrets looks up the target secret and makes sure the target secret exists and contains annotations +func (whsvr *WebhookServer) ensureSecrets(ar *v1beta1.AdmissionReview) error { + glog.Infof("Ensuring existing secrets") + targetNamespace := ar.Request.Namespace + + glog.Infof("Looking for the existing target secret") + secret, err := whsvr.client.CoreV1().Secrets(targetNamespace).Get(whsvr.config.targetSecretName, metav1.GetOptions{}) + if err != nil && !errors.IsNotFound(err) { + glog.Errorf("Could not fetch secret %s in namespace %s: %v", whsvr.config.targetSecretName, targetNamespace, err) + return err + } + + if err != nil && errors.IsNotFound(err) { + glog.Infof("Target secret not found, creating a new one") + if _, createErr := whsvr.client.CoreV1().Secrets(targetNamespace).Create(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: whsvr.config.targetSecretName, + Namespace: targetNamespace, + }, + Data: nil, + Type: corev1.SecretTypeOpaque, + }); createErr != nil { + glog.Errorf("Could not create secret %s in namespace %s: %v", whsvr.config.targetSecretName, targetNamespace, err) + return err + } + glog.Infof("Target secret created successfully") + return nil + } + + glog.Infof("Target secret found, updating") + annotationData := strings.Split(whsvr.config.targetSecretAnnotation, "=") + secret.Annotations[annotationData[0]] = annotationData[1] + if _, err := whsvr.client.CoreV1().Secrets(targetNamespace).Update(secret); err != nil { + glog.Errorf("Could not update secret %s in namespace %s: %v", whsvr.config.targetSecretName, targetNamespace, err) + return err + } + glog.Infof("Target secret updated successfully") + + return nil +} + +// shouldMutate goes through all filters and determines whether the incoming NS matches them +func (whsvr *WebhookServer) shouldMutate(ns corev1.Namespace) bool { + for _, excludedNamespace := range strings.Split(whsvr.config.excludeNamespaces, ",") { + if ns.Name == excludedNamespace { + return false + } + } + + return true +} + +// mutateNamespace contains the whole mutation logic +func (whsvr *WebhookServer) mutateNamespace(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + req := ar.Request + glog.Infof("Unmarshalling the Namespace object from request") + var ns corev1.Namespace + if err := json.Unmarshal(req.Object.Raw, &ns); err != nil { + glog.Errorf("Could not unmarshal raw object: %v", err) + return &v1beta1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + } + } + + glog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v (%v) UID=%v patchOperation=%v UserInfo=%v", + req.Kind, req.Namespace, req.Name, ns.Name, req.UID, req.Operation, req.UserInfo) + + if !whsvr.shouldMutate(ns) { + glog.Infof("Conditions for mutation not met, skipping") + return &v1beta1.AdmissionResponse{ + Allowed: true, + } + } + secretList, err := whsvr.client.CoreV1().Secrets(ns.Name).List(metav1.ListOptions{ + Watch: false, + }) + if err != nil { + glog.Errorf("Could not get secret from namespace: %v", err) + return &v1beta1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + } + } + + // Check whether we already have configured secret with annotation present + if secretList != nil { + for _, item := range secretList.Items { + if item.Name == whsvr.config.targetSecretName { + annotationToCheck := strings.Split(whsvr.config.targetSecretAnnotation, "=") + if val, ok := item.Annotations[annotationToCheck[0]]; ok { + glog.Infof("Namespace is already in the correct state and contains secret %s with value %s=%s, skipping", whsvr.config.targetSecretName, annotationToCheck ,val) + return &v1beta1.AdmissionResponse{ + Allowed: true, + } + } + } + } + } + + if err := whsvr.ensureSecrets(ar); err != nil { + glog.Errorf("Could not ensure existence of the secret") + glog.Errorf("Failing the mutation process") + return &v1beta1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + } + } + + return &v1beta1.AdmissionResponse{ + Allowed: true, + Patch: nil, + PatchType: nil, + } +} + +func parseIncomingRequest(r *http.Request) (v1beta1.AdmissionReview, *errors.StatusError) { + var ar v1beta1.AdmissionReview + var body []byte + if r.Body != nil { + if data, err := ioutil.ReadAll(r.Body); err == nil { + body = data + } + } + if len(body) == 0 { + glog.Error("Empty body") + return ar, errors.NewBadRequest("Empty body") + } + + // verify the content type is accurate + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + glog.Errorf("Content-Type=%s, expect application/json", contentType) + err := &errors.StatusError{ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Message: fmt.Sprintf("Content-Type=%s, expect application/json", contentType), + Reason: metav1.StatusReasonUnsupportedMediaType, + Code: http.StatusUnsupportedMediaType, + }} + return ar, err + } + + if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { + glog.Error("Could not parse the request body") + return ar, errors.NewBadRequest(fmt.Sprintf("Could not parse the request body: %+v", err)) + } + + return ar, nil +} + +func (whsvr *WebhookServer) sendResponse(w http.ResponseWriter, admissionReview v1beta1.AdmissionReview) error { + resp, err := json.Marshal(admissionReview) + if err != nil { + glog.Errorf("Can't encode response: %v", err) + http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) + return err + } + glog.Infof("Writing response") + if _, err := w.Write(resp); err != nil { + glog.Errorf("Can't write response: %v", err) + http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) + return err + } + + return nil +} + +// serve parses the raw incoming request, calls the mutation logic and sends the proper response +func (whsvr *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { + admissionReviewIn, statusErr := parseIncomingRequest(r) + if statusErr != nil { + http.Error(w, statusErr.ErrStatus.Message, int(statusErr.ErrStatus.Code)) + return + } + + admissionResponse := whsvr.mutateNamespace(&admissionReviewIn) + + admissionReviewOut := v1beta1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{Kind: "AdmissionReview", APIVersion: "admission.k8s.io/v1"}, + } + if admissionResponse != nil { + admissionReviewOut.Response = admissionResponse + if admissionReviewIn.Request != nil { + admissionReviewOut.Response.UID = admissionReviewIn.Request.UID + } + } + + if err := whsvr.sendResponse(w, admissionReviewOut); err != nil { + glog.Errorf("Could not send response %v", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f9bc33e --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/ysoftdevs/imagepullsecret-injector + +require ( + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + k8s.io/api v0.17.0 + k8s.io/apimachinery v0.17.0 + k8s.io/client-go v0.17.0 +) + +go 1.15 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..82256c3 --- /dev/null +++ b/go.sum @@ -0,0 +1,189 @@ +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-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/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +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/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/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/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.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 v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.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.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 h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +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/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/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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +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-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +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.10.1/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/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/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= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +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-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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/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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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/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-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/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-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-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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +k8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM= +k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= +k8s.io/apimachinery v0.17.0 h1:xRBnuie9rXcPxUkDizUsGvPf1cnlZCFu210op7J7LJo= +k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg= +k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/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-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=