Repository: tuenti/secrets-manager Branch: master Commit: 3fd9e760532d Files: 77 Total size: 278.1 KB Directory structure: gitextract_hher7_ul/ ├── .circleci/ │ └── config.yml ├── .dockerignore ├── .github/ │ └── pull_request_template.md ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api/ │ └── v1alpha1/ │ ├── groupversion_info.go │ ├── secretdefinition_types.go │ ├── secretdefinition_types_test.go │ ├── suite_test.go │ └── zz_generated.deepcopy.go ├── backend/ │ ├── azure_kv.go │ ├── azure_kv_metrics.go │ ├── azure_kv_metrics_test.go │ ├── azure_kv_test.go │ ├── backend.go │ ├── backend_test.go │ ├── decoder.go │ ├── decoder_test.go │ ├── vault.go │ ├── vault_engine.go │ ├── vault_engine_test.go │ ├── vault_metrics.go │ ├── vault_metrics_test.go │ └── vault_test.go ├── config/ │ ├── crd/ │ │ ├── bases/ │ │ │ └── secrets-manager.tuenti.io_secretdefinitions.yaml │ │ ├── kustomization.yaml │ │ ├── kustomizeconfig.yaml │ │ └── patches/ │ │ ├── cainjection_in_secretdefinitions.yaml │ │ └── webhook_in_secretdefinitions.yaml │ ├── default/ │ │ ├── kustomization.yaml │ │ ├── manager_auth_proxy_patch.yaml │ │ ├── manager_config_patch.yaml │ │ └── manager_image_patch.yaml │ ├── manager/ │ │ ├── controller_manager_config.yaml │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── prometheus/ │ │ ├── kustomization.yaml │ │ └── monitor.yaml │ ├── rbac/ │ │ ├── auth_proxy_client_clusterrole.yaml │ │ ├── auth_proxy_role.yaml │ │ ├── auth_proxy_role_binding.yaml │ │ ├── auth_proxy_service.yaml │ │ ├── kustomization.yaml │ │ ├── leader_election_role.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── role.yaml │ │ ├── role_binding.yaml │ │ ├── secretdefinition_editor_role.yaml │ │ ├── secretdefinition_viewer_role.yaml │ │ └── service_account.yaml │ └── samples/ │ ├── README.md │ ├── crd.yaml │ ├── secrets-manager.yaml │ ├── secretsmanager_v1alpha1_secretdefinition.yaml │ ├── vault-setup.sh │ └── vault.yaml ├── controllers/ │ ├── metrics.go │ ├── secretdefinition_controller.go │ ├── secretdefinition_controller_test.go │ └── suite_test.go ├── deploy/ │ ├── Dockerfile │ └── version/ │ ├── get.sh │ ├── update.sh │ └── version.properties ├── docker-compose.yaml ├── errors/ │ ├── errors.go │ └── errors_test.go ├── go.mod ├── go.sum ├── hack/ │ └── boilerplate.go.txt ├── main.go └── scripts/ └── setup-dev-env.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ --- version: 2.0 jobs: unit_tests: docker: - image: circleci/golang:1.16 environment: KUBEBUILDER_CONTROLPLANE_START_TIMEOUT: "60s" steps: - checkout - setup_remote_docker - run: make test - run: command: bash <(curl -s https://codecov.io/bash) when: always docker_hub_master: docker: - image: circleci/golang:1.16 environment: KUBEBUILDER_CONTROLPLANE_START_TIMEOUT: "60s" steps: - checkout - setup_remote_docker - run: make test docker_hub_release_tags: docker: - image: circleci/golang:1.16 environment: KUBEBUILDER_CONTROLPLANE_START_TIMEOUT: "60s" steps: - checkout - setup_remote_docker - run: make test workflows: version: 2 secrets-manager: jobs: - unit_tests - docker_hub_master: requires: - unit_tests filters: branches: only: master - docker_hub_release_tags: requires: - unit_tests filters: tags: only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ branches: ignore: /.*/ ================================================ FILE: .dockerignore ================================================ vendor/ build/ Dockerfile .git .gitignore ================================================ FILE: .github/pull_request_template.md ================================================ # Status READY/IN DEVELOPMENT/HOLD # Migrations YES (describe migration) | NO # Description A few sentences describing the overall goals of the pull request's commits. # List of fixes # (issue) - fix #X - fix #N # Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration # Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib bin testbin # Tarballs *.tar* # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Skip vendored files !vendor/**/zz_generated.* # editor and IDE paraphernalia .vscode .idea *.swp *.swo *~ secrets-manager ================================================ FILE: CHANGELOG.md ================================================ ## Unreleased - [FEATURE] Add support for Azure KeyVault backend ## v2.0.1 2022-04-04 - [BUG] Fix nil pointer dereference bug in controller's regular kubernetes client ## v2.0.0 2022-02-21 - [FEATURE] Populating Labels and Annotations from the SecretDefinition to the generated Secret. - [ENHANCEMENT] Updates the `managed-by` and `updatedAt` labels to more closely match k8s recommended values (using annotations and recommended labels), as seen below: ```yaml annotations: secrets-manager.tuenti.io/lastUpdateTime: 2020-04-22T14.34.17Z labels: app.kubernetes.io/managed-by: secrets-manager ``` - [ENHANCEMENT] Update to kubebuilder 3.1.0 ## v1.1.0 2021-01-05 - [BEHAVIOUR] Using flags watch-namespaces / exclude-namespaces. They interact differently. - All namespaces are watched. A namespace is excluded if it is specified within the *exclude-namespaces* flag. - [FEATURE] Adding **auth-method** param to specify Vault authentication method. - Adding vault authentication method from kubernetes. With **auth-method** param set to **kubernetes**. - [BUG] set the controller name to something unique avoid 'duplicate metrics collector registration attempted' errors. ## no code related changes 2020-04-28 - No logic changes in secrets-manager. But we are going to stablish some changes in the project management: - Now versions are going to follow [semantic versioning](https://semver.org/) where version tags are going to have the 'v' preffix, they are going to be just: - v{major}.{minor}.{patch}, where major, minor and path are integers - From now on we are going to push release candidates to the [docker registry](https://hub.docker.com/repository/docker/tuentitech/secrets-manager) ## v1.0.2 2019-11-17 Stable release. Adds watching specific namespaces (see v1.0.2-rc.1) and some minor fixes. ### Fixes - [#47 missing return provokes wrong metrics delivery](https://github.com/tuenti/secrets-manager/issues/47) - [#37 Unable to build 1.0.1](https://github.com/tuenti/secrets-manager/issues/37) ## v1.0.2-rc.1 2019-09-30 ### Fixes - [#38 add the ability to watch secretDefinitions scoped to a particular namespace](https://github.com/tuenti/secrets-manager/issues/38) ## v1.0.1 2019-08-14 ### Fixes - Deleting a `SecretDefinition` hangs if the corresponding secret does not exist. - Invalid metric names in README ### Deprecates - Unused prometheus metrics `secrets_manager_controller_update_secret_errors_total` and `secrets_manager_controller_last_updated` ## v1.0.0 2019-07-29 Stable release ## v1.0.0-rc.1 2019-07-12 Release Candidate 1 ## v1.0.0-snapshot-1 2019-07-09 ### Added - `SecretDefinitions` created via `CustomResourceDefinitions` - If the `SecretDefinion` gets deleted, the corresponding secret will be removed too. - New zap logger based on [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) project. Use `-enable-debug-log` to get a more verbose output. ### Fixes - [#2 Switch to custom resource definitions instead of a single configmap](https://github.com/tuenti/secrets-manager/issues/2) - [#8 Secrets deletion proposal](https://github.com/tuenti/secrets-manager/issues/8) ### Breaking changes - congimaps won't be supported to define secrets, and so that won't work all the relevant configmap flags. - log.format and log.level flags won't work anymore, as we have changed the logger to addapt to the [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) project. Use `-enable-debug-log` to get a more verbose output. - `config.backend-scrape-interval` no longer works as we check the backend state on every reconcile event. Use `reconcile-period` instead - `listen-address` removed in favor of `metrics-addr` ## v1.0.0-snapshot 2019-05-22 ### Added - Enable Vault AppRole auth method and `secrets-manager` will try to re-login every time it fails to fetch the token. This will make `secrets-manager` more resilient to issues connecting to Vault that potentially caused the token to expire. - New `secrets_manager_login_errors_total` Prometheus metric. ### Fixes - [#27-Implement AppRole auth](https://github.com/tuenti/secrets-manager/issues/27) ### Breaking changes - Token based login won't be supported, as re-login with and invalid token won't make `secrets-manager` to self-heal. - This makes this new version not backward compatible with previous v0.2.0 ## v0.2.0 - 2019-03-29 Stable ## v0.2.0-rc.2 - 2019-01-29 ### Added - New `secrets_manager_vault_max_token_ttl` metric, so a user could alert based on this and `secrets_manager_token_ttl` - New `secrets_manager_secret_last_sync_status` metric, that shows wether the secret succeeded or not in last synchronization iteration ### Fixed - Backend timeout not properly set through flags - Deprecates `secrets_manager_vault_token_expired` metric as it was quite confusing since it's not really possible for `secrets-manager` to know when the token it's expired, just when it's "close to expire". - Renames counter metrics to follow the Prometheus naming standard with the `_total` suffix instead of `_count`. - Simplifies prometheus token renewal metrics by merging `secrets_manager_vault_token_lookup_errors_count` and `secrets_manager_vault_token_renew_errors_count` into one single metric `secrets_manager_vault_token_renewal_errors_total` with one more dimension called `vault_operation` which will be one of `lookup-self, renew-self, is-renewable`. ## v0.2.0-rc.1 - 2019-01-21 ### Added - Enable prometheus metrics - `cfg.backend-timeout` flag to specify a connection timeout to the secrets backend. - `listen-address` flag to specify the listen address of the HTTP API ### Fixed - Bad return condition in startTokenRenewer, so token lookup won't happen in case of a token revoked. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to secrets-manager If you find something missing or not working as expected, we are happy to receive your pull requests! These are the guidelines to follow to make your awesome code part of _secrets-manager_ ## Steps to Contribute * Create an Issue * Fork _secrets-manager_ and work on the new feature/bugfix * Open a Pull Request to propose your changes: * New features: into master * Bugfixes: into latest release branch ## Git Branches Model We have 4 kind of branches in _secrets-manager_ development: * **`master`**: is the integration branch, new features are merged into this. Developers create their feature branch and open a Pull Request to master to propose the changes. * **`release-*` branches**: are used to prepare every new _secrets-manager_ minor release (`.`, ie: 0.1, 1.2, etc). In these branches we don't merge new features, only bugfixes. Once a new bug is fixed in the latest release branch, it has to be merged into `master` too. * **feature branches**: branches created by developers to implement new functinalities in _secrets-manager_. They can only be merged into master. * **bugfix branches**: branches with fixes for bugs, they can only be merged into release branches. Given this, we use `master` as our integration branch and maintain separated branches for each minor release. New features should be merged into `master` branch, and bugfixes should be merged into `release-*` branches. For a patch release, work on the corresponding minor release branch. ### Publishing the release Once the work in the release branch is stabilized, create a tag in the branch. ### Release Candidates Release candidates are treated as normal releases, but they must append `-rc[0-9]*` to the branch name. ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: Makefile ================================================ DOCKER_REGISTRY = "registry.hub.docker.com" ORGANIZATION = "tuentitech" BINARY_NAME=secrets-manager VERSION=$(shell deploy/version/get.sh) BUILD_FLAGS=-ldflags "-X main.version=${VERSION}" # Image URL to use all building/pushing image targets IMG = ${DOCKER_REGISTRY}/${ORGANIZATION}/${BINARY_NAME}:${VERSION} # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false" # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else GOBIN=$(shell go env GOBIN) endif # Setting SHELL to bash allows bash commands to be executed by recipes. # This is a requirement for 'setup-envtest.sh' in the test target. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec all: build ##@ General # The help target prints out all targets with their descriptions organized # beneath their categories. The categories are represented by '##@' and the # target descriptions by '##'. The awk commands is responsible for reading the # entire set of makefiles included in this invocation, looking for lines of the # file as xyz: ## something, and then pretty-format the target and help. Then, # if there's a line with ##@ something, that gets pretty-printed as a category. # More info on the usage of ANSI control characters for terminal formatting: # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters # More info on the awk command: # http://linuxcommand.org/lc3_adv_awk.php help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) ##@ Development manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." fmt: ## Run go fmt against code. go fmt ./... vet: ## Run go vet against code. go vet ./... ENVTEST_ASSETS_DIR=$(shell pwd)/testbin test: manifests generate fmt vet ## Run tests. mkdir -p ${ENVTEST_ASSETS_DIR} test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.8.3/hack/setup-envtest.sh source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out ##@ Build build: generate fmt vet ## Build manager binary. go build ${BUILD_FLAGS} -o bin/${BINARY_NAME} main.go run: manifests generate fmt vet ## Run a controller from your host. go run ./main.go # Run tests in docker docker-test: docker-compose run tests # Build release docker image docker-build: docker-test docker build . \ --file ./deploy/Dockerfile \ --target release \ --build-arg SECRETS_MANAGER_VERSION=${VERSION} \ --tag ${IMG} @echo "updating kustomize image patch file for manager resource" sed -i'' -e 's@image: .*@image: '"${IMG}"'@' ./config/default/manager_image_patch.yaml # Push the docker image docker-push: docker push ${IMG} update-major-version: deploy/version/update.sh --major update-minor-version: deploy/version/update.sh --minor update-patch-version: deploy/version/update.sh --patch ##@ Deployment install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/crd | kubectl apply -f - uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/crd | kubectl delete -f - deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} $(KUSTOMIZE) build config/default | kubectl apply -f - undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/default | kubectl delete -f - CONTROLLER_GEN = $(shell pwd)/bin/controller-gen controller-gen: ## Download controller-gen locally if necessary. $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.1) KUSTOMIZE = $(shell pwd)/bin/kustomize kustomize: ## Download kustomize locally if necessary. $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) # go-get-tool will 'go get' any package $2 and install it to $1. PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) define go-get-tool @[ -f $(1) ] || { \ set -e ;\ TMP_DIR=$$(mktemp -d) ;\ cd $$TMP_DIR ;\ go mod init tmp ;\ echo "Downloading $(2)" ;\ GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ rm -rf $$TMP_DIR ;\ } endef ================================================ FILE: PROJECT ================================================ domain: secrets-manager.tuenti.io layout: - go.kubebuilder.io/v3 projectName: secrets-manager repo: github.com/tuenti/secrets-manager resources: - api: crdVersion: v1 namespaced: true controller: true domain: secrets-manager.tuenti.io group: secretsmanager kind: SecretDefinition path: github.com/tuenti/secrets-manager/api/v1alpha1 version: v1alpha1 version: "3" ================================================ FILE: README.md ================================================ # secrets-manager [![CircleCI](https://circleci.com/gh/tuenti/secrets-manager/tree/master.svg?style=svg)](https://circleci.com/gh/tuenti/secrets-manager/tree/master) [![Go Report Card](https://goreportcard.com/badge/github.com/tuenti/secrets-manager)](https://goreportcard.com/report/github.com/tuenti/secrets-manager) [![codecov](https://codecov.io/gh/tuenti/secrets-manager/branch/master/graph/badge.svg)](https://codecov.io/gh/tuenti/secrets-manager) A tool to keep your Kubernetes secrets in sync with Vault # Rationale Lots of companies use [Vault](https://www.vaultproject.io) as their secrets store backend for multiple kind of secrets and different purposes. Kubernetes brings a nice secrets API, but it means that you have two different sources of truth for your secrets. *secrets-manager* tries to solve this, by reading secrets from Vault and comparing them to Kubernetes secrets, creating and updating them as you do it in Vault. # How does it compare to other tools? - [cert-manager](https://github.com/jetstack/cert-manager). *cert-manager* solves a different issue, automation around issuing and renewing certificates. It integrates with Let's Encrypt and Vault (using the pki backend) being those the certificates issuer. While this is really powerful and really a tool which is fully compatible with *secrets-manager*, it does not really sync a secret from a secret backend. *secrets-manager* is a more generic tool where you can sync certificates or any kind of secret from the source of truth of your secrets to Kubernetes secrets. - [vault-operator](https://github.com/coreos/vault-operator). This manages vault clusters in Kubernetes, so it is a completely different tool. - [vault-crd](https://github.com/DaspawnW/vault-crd). This is the tool that really inspired *secrets-manager*. We opened this [issue](https://github.com/DaspawnW/vault-crd/issues/4) asking for token renewal or other login mechanism. While the author is very responsive answering, we could not wait for an implementation and since we were are more familiar with Go than Java we decided to write *secrets-manager*. We are very thankful to the author of *vault-crd*, since it has been really inspiring. Some differences: - *vault-crd* only supports Hashicorp Vault as its secrets manager, while *secrets-manager* has been designed to support other backends (we only support Vault for now, though). - *vault-crd* supports KV1, KV2 and PKI secret engines. *secrets-manager* supports KV1 and KV2. It is on our roadmap to support more secret engines. # How it works *secrets-manager* will login to Vault using AppRole credentials and it will start a reconciliation loop watching for changes in `SecretsDefinition` objects. In background it will run two main operations: - If Vault token is close to expire and if that's the case, renewing it. If it can't renew, it will try to re-login. - It will re-queue `SecretsDefinition` events and in every event loop it will verify if the current Kubernetes secret it is in the desired state by comparing it with the data in Vault and creating/updating them accordingly ## Custom Resource Definition (CRD) *secrets-manager* now uses [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) to extend Kubernetes APIs with a new `SecretDefinition` object that it will watch. To install the CRD in your cluster: `kubectl apply -f crd.yaml` ### Secrets Definition - `name`: This will be the name of the secret created in Kubernetes. - `type`: Kubernetes secret type. One of `kubernetes.io/tls`, `Opaque`. - `keysMap`: This will contain the Kubernetes secret data keys as a map of datasources. Each datasource will contain the way to access the secret in the secret backend source of truth, via a `path` and a `key`. And optional `encoding` key can be provided if your secrets are codified in `base64`. The absence of `encoding` or `encoding: text` means no encoding. **NOTE**: We let the user all the responsibility to set the whole Vault path. So it is important to know which path a secret engine needs to be set. For instance, with the KV version 1 all secrets are stored in `secret/` whereas with the KV version 2, all secrets go under `secret/data/` An example of a `secretdefinition` object ``` $ cat > secretdefinition-sample.yaml < Using RBAC Authorization > Aggregated ClusterRoles](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles). ## Prometheus Metrics `secrets-manager` exposes the following [Prometheus](https://prometheus.io) metrics at `http://$cfg.listen-addr/metrics`: | Metric| Type| Description| Labels| | ------| ----|------------| ------| |`secrets_manager_vault_max_token_ttl` | Gauge | `secrets-manager` max Vault token TTL | `"vault_address", "vault_engine", "vault_version", "vault_cluster_id", "vault_cluster_name"` | |`secrets_manager_vault_token_ttl` | Gauge | Vault token TTL | `"vault_address", "vault_engine", "vault_version", "vault_cluster_id", "vault_cluster_name"` | |`secrets_manager_vault_token_renewal_errors_total`| Counter | Vault token renewal errors counter | `"vault_address", "vault_engine", "vault_version", "vault_cluster_id", "vault_cluster_name", "vault_operation", "error"` | |`secrets_manager_controller_secret_read_errors_total`| Counter | Errors total count when reading a secret from Kubernetes | `"name", "namespace"` | | `secrets_manager_controller_sync_errors_total`| Counter |Secrets synchronization total errors.|`"name", "namespace"`| |`secrets_manager_controller_last_sync_status`| Gauge |The result of the last sync of a secret. 1 = OK, 0 = Error|`"name", "namespace"`| ## Getting Started with Vault ### Vault Policies We do recommend you use policies to make sure you grant `secrets-manager` only to those secrets you need available in your Kubernetes cluster. An example of simple policy could be: ``` path "secret/data/my-k8s-cluster/*" { capabilities = ["read"] } ``` To create this policy: ``` $ cat > my-policy.hcl < secrets-manager-role.json < --name --resource-group ``` ### Azure authentication methods Secrets manager currently supports the following authentication methods for Azure: - [*Azure Managed Identity*](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview). This is the preferred method if secrets manager is running in an Azure VM or Azure Kubernetes Service (AKS) cluster. For further information about how to use managed identities in Azure Kubernetes Service (AKS) see [documentation](https://docs.microsoft.com/en-us/azure/aks/use-managed-identity). - [*Azure Service Principal*](https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals). This method can be used if secrets manager runs outside of Azure service, although it requires more configuration steps. ### Create a Service Principal to access secrets `secrets-manager` uses Azure Service Principal to authenticate against Azure KeyVault API. It's recommended to use an isolated Service Principal to limit its access to the least required resources: ``` $ az ad sp create-for-rbac --name "" --role Contributor --scopes /subscriptions/{SubID}/resourceGroups/{ResourceGroup} ``` This command will output a JSON object like the following: ``` { "appId": "", // ClientId "displayName": "", "name": "http://", "password": "", // ClientSecret "tenant": "" } ``` The fields needed by `secrets-manager` to authenticate are `appId` (`azure-kv.client-id`), `password` (`azure-kv.client-secret`) and `tenant` (`azure-kv.tenant-id`). Once the Service Principal is created, add permission to access Azure KeyVault's secrets with: ``` $ az keyvault set-policy --name --spn --secret-permissions get list set delete ``` ## Versioning Right now versioning it's a manually task. Depending on the kind of the update we would apply a major, minor or patch update given that we follow [semantic versioning](https://semver.org/). Before building release images, we should run one of the following commands: - make update-major-version - make update-minor-version - make update-patch-version ## Deployment *secrets-manager* has been designed to be deployed in Kubernetes, you will find a full deployment example in the [config/samples](config/samples) folder. ## Credits & Contact *secrets-manager* is developed and maintained by [Tuenti Technologies S.L.](http://github.com/tuenti) You can follow Tuenti engineering team on Twitter [@tuentieng](http://twitter.com/tuentieng). ## License *secrets-manager* is available under the Apache License, Version 2.0. See LICENSE file for more info. ================================================ FILE: api/v1alpha1/groupversion_info.go ================================================ /* Copyright 2021. 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 contains API Schema definitions for the secretsmanager v1alpha1 API group //+kubebuilder:object:generate=true //+groupName=secrets-manager.tuenti.io package v1alpha1 import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) const ( Group = "secrets-manager.tuenti.io" Version = "v1alpha1" ) var ( // GroupVersion is group version used to register these objects GroupVersion = schema.GroupVersion{Group: Group, Version: Version} // 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 ) ================================================ FILE: api/v1alpha1/secretdefinition_types.go ================================================ /* 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 ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // DataSource represents the actual source of truth path for a secret type DataSource struct { // Path to the actual secret Path string `json:"path"` // Key where the actual secret is stored Key string `json:"key"` // Encoding type for the secret. Only base64 supported. Optional Encoding string `json:"encoding,omitempty"` } // SecretDefinitionSpec defines the desired state of SecretDefinition type SecretDefinitionSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file Name string `json:"name"` Type string `json:"type,omitempty"` KeysMap map[string]DataSource `json:"keysMap"` } // SecretDefinitionStatus defines the observed state of SecretDefinition type SecretDefinitionStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file } // +kubebuilder:object:root=true // SecretDefinition is the Schema for the secretdefinitions API type SecretDefinition struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec SecretDefinitionSpec `json:"spec,omitempty"` Status SecretDefinitionStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // SecretDefinitionList contains a list of SecretDefinition type SecretDefinitionList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []SecretDefinition `json:"items"` } func init() { SchemeBuilder.Register(&SecretDefinition{}, &SecretDefinitionList{}) } ================================================ FILE: api/v1alpha1/secretdefinition_types_test.go ================================================ package v1alpha1 /* 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. */ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "golang.org/x/net/context" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) // These tests are written in BDD-style using Ginkgo framework. Refer to // http://onsi.github.io/ginkgo to learn more. var _ = Describe("SecretDefinition", func() { var ( key types.NamespacedName created, fetched *SecretDefinition ) BeforeEach(func() { // Add any setup steps that needs to be executed before each test }) AfterEach(func() { // Add any teardown steps that needs to be executed after each test }) // Add Tests for OpenAPI validation (or additional CRD features) specified in // your API definition. // Avoid adding tests for vanilla CRUD operations because they would // test Kubernetes API server, which isn't the goal here. Context("Create API", func() { It("should create an object successfully", func() { key = types.NamespacedName{ Name: "foo", Namespace: "default", } created = &SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "default", }, Spec: SecretDefinitionSpec{ Name: "foo", Type: "Opaque", KeysMap: map[string]DataSource{ "foo": { Path: "secret/supersecret1", Key: "foo", Encoding: "text", }, }, }, } By("creating an API obj") Expect(k8sClient.Create(context.TODO(), created)).To(Succeed()) fetched = &SecretDefinition{} Expect(k8sClient.Get(context.TODO(), key, fetched)).To(Succeed()) Expect(fetched).To(Equal(created)) By("deleting the created object") Expect(k8sClient.Delete(context.TODO(), created)).To(Succeed()) Expect(k8sClient.Get(context.TODO(), key, created)).ToNot(Succeed()) }) }) }) ================================================ FILE: api/v1alpha1/suite_test.go ================================================ /* 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 ( "path/filepath" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) RunSpecsWithDefaultAndCustomReporters(t, "v1alpha1 Suite", []Reporter{printer.NewlineReporter{}}) } var _ = BeforeSuite(func(done Done) { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, } err := SchemeBuilder.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) cfg, err = testEnv.Start() Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).ToNot(HaveOccurred()) Expect(k8sClient).ToNot(BeNil()) close(done) }, 60) var _ = AfterSuite(func() { By("tearing down the test environment") err := testEnv.Stop() Expect(err).ToNot(HaveOccurred()) }) ================================================ FILE: api/v1alpha1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright 2021. 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 ( 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 *DataSource) DeepCopyInto(out *DataSource) { *out = *in } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSource. func (in *DataSource) DeepCopy() *DataSource { if in == nil { return nil } out := new(DataSource) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretDefinition) DeepCopyInto(out *SecretDefinition) { *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 SecretDefinition. func (in *SecretDefinition) DeepCopy() *SecretDefinition { if in == nil { return nil } out := new(SecretDefinition) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *SecretDefinition) 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 *SecretDefinitionList) DeepCopyInto(out *SecretDefinitionList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]SecretDefinition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretDefinitionList. func (in *SecretDefinitionList) DeepCopy() *SecretDefinitionList { if in == nil { return nil } out := new(SecretDefinitionList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *SecretDefinitionList) 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 *SecretDefinitionSpec) DeepCopyInto(out *SecretDefinitionSpec) { *out = *in if in.KeysMap != nil { in, out := &in.KeysMap, &out.KeysMap *out = make(map[string]DataSource, len(*in)) for key, val := range *in { (*out)[key] = val } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretDefinitionSpec. func (in *SecretDefinitionSpec) DeepCopy() *SecretDefinitionSpec { if in == nil { return nil } out := new(SecretDefinitionSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretDefinitionStatus) DeepCopyInto(out *SecretDefinitionStatus) { *out = *in } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretDefinitionStatus. func (in *SecretDefinitionStatus) DeepCopy() *SecretDefinitionStatus { if in == nil { return nil } out := new(SecretDefinitionStatus) in.DeepCopyInto(out) return out } ================================================ FILE: backend/azure_kv.go ================================================ package backend import ( "context" goerrors "errors" "fmt" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" "github.com/go-logr/logr" "github.com/tuenti/secrets-manager/errors" ) var akvMetrics *azureKVMetrics const ( azureKVEndpoint = "vault.azure.net" ) type azureKVClient struct { client *azsecrets.Client keyvaultName string context context.Context logger logr.Logger } // getAzureCredential finds the better way to authenticate to Azure func getAzureCredential(ctx context.Context, logger logr.Logger, cfg Config) (azcore.TokenCredential, error) { if cfg.AzureKVManagedClientID != "" || cfg.AzureKVManagedResourceID != "" { opts := azidentity.ManagedIdentityCredentialOptions{} if cfg.AzureKVManagedClientID != "" { opts.ID = azidentity.ClientID(cfg.AzureKVManagedClientID) } else if cfg.AzureKVManagedResourceID != "" { opts.ID = azidentity.ResourceID(cfg.AzureKVManagedResourceID) } managed, err := azidentity.NewManagedIdentityCredential(&opts) if err == nil { logger.Info("Azure Managed Identity will be used as authentication method") return managed, err } } spSecret, err := azidentity.NewClientSecretCredential(cfg.AzureKVTenantID, cfg.AzureKVClientID, cfg.AzureKVClientSecret, nil) if err == nil { logger.Info("Azure Service Principal will be used as authentication method") return spSecret, err } return nil, goerrors.New("Unable to authenticate to Azure API using any method") } func azureKeyVaultClient(ctx context.Context, l logr.Logger, cfg Config) (*azureKVClient, error) { logger := l.WithName("azure-kv").WithValues( "azure_kv_name", cfg.AzureKVName, "azure_kv_tenant", cfg.AzureKVTenantID) cred, err := getAzureCredential(ctx, logger, cfg) if err != nil { logger.Error(err, "Error while authenticating to Azure") return nil, err } akvMetrics = newAzureKVMetrics(cfg.AzureKVName, cfg.AzureKVTenantID) vaultEndpoint := fmt.Sprintf("https://%s.%s", cfg.AzureKVName, azureKVEndpoint) akvClient, err := azsecrets.NewClient(vaultEndpoint, cred, nil) if err != nil { logger.Error(err, "Error while creating Azure KV client") akvMetrics.updateLoginErrorsTotalMetric() return nil, err } logger.Info("Successfully logged into Azure KeyVault") client := azureKVClient{ client: akvClient, keyvaultName: cfg.AzureKVName, context: ctx, logger: logger, } return &client, err } func (c *azureKVClient) ReadSecret(path string, key string) (string, error) { data := "" // TODO: Add support for secret version? result, err := c.client.GetSecret(c.context, path, nil) if err != nil { errorType := errors.UnknownErrorType var responseError *azcore.ResponseError if goerrors.As(err, &responseError) { if responseError.StatusCode == 404 { errorType = errors.BackendSecretNotFoundErrorType } if responseError.StatusCode == 403 { errorType = errors.BackendSecretForbiddenErrorType } } akvMetrics.updateSecretReadErrorsTotalMetric(path, errorType) return data, err } data = *result.Value return data, err } ================================================ FILE: backend/azure_kv_metrics.go ================================================ package backend import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( azureKVLabelNames = []string{"azure_kv_name", "azure_kv_tenant"} azureKVSecretReadErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: "secrets_manager", Subsystem: "azure_kv", Name: "read_secret_errors_total", Help: "AzureKV read operations counter", }, append(azureKVLabelNames, secretLabelNames...)) azureKVLoginErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: "secrets_manager", Subsystem: "azure_kv", Name: "login_errors_total", Help: "AzureKV login errors counter", }, azureKVLabelNames) ) type azureKVMetrics struct { labels map[string]string } func newAzureKVMetrics(keyvaultName string, tenantID string) *azureKVMetrics { labels := make(map[string]string, len(azureKVLabelNames)) labels["azure_kv_name"] = keyvaultName labels["azure_kv_tenant"] = tenantID return &azureKVMetrics{labels: labels} } func (vm *azureKVMetrics) updateSecretReadErrorsTotalMetric(path string, errorType string) { azureKVSecretReadErrorsTotal.WithLabelValues( vm.labels["azure_kv_name"], vm.labels["azure_kv_tenant"], path, "", errorType, ).Inc() } func (vm *azureKVMetrics) updateLoginErrorsTotalMetric() { azureKVLoginErrorsTotal.WithLabelValues( vm.labels["azure_kv_name"], vm.labels["azure_kv_tenant"], ).Inc() } ================================================ FILE: backend/azure_kv_metrics_test.go ================================================ package backend import ( "testing" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/tuenti/secrets-manager/errors" ) func TestAzureKVUpdateLoginErrorsTotal(t *testing.T) { metrics := newAzureKVMetrics(fakeKeyVaultName, fakeKeyVaultTenant) azureKVLoginErrorsTotal.Reset() metrics.updateLoginErrorsTotalMetric() metricLoginErrors, _ := azureKVLoginErrorsTotal.GetMetricWithLabelValues(fakeKeyVaultName, fakeKeyVaultTenant) assert.Equal(t, 1.0, testutil.ToFloat64(metricLoginErrors)) } func TestAzureKVUpdateReadSecretErrorsTotal(t *testing.T) { path := "/path/to/secret" key := "" metrics := newAzureKVMetrics(fakeKeyVaultName, fakeKeyVaultTenant) azureKVSecretReadErrorsTotal.Reset() metrics.updateSecretReadErrorsTotalMetric(path, errors.UnknownErrorType) metricSecretReadErrorsTotal, _ := azureKVSecretReadErrorsTotal.GetMetricWithLabelValues(fakeKeyVaultName, fakeKeyVaultTenant, path, key, errors.UnknownErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricSecretReadErrorsTotal)) azureKVSecretReadErrorsTotal.Reset() metrics.updateSecretReadErrorsTotalMetric(path, errors.BackendSecretNotFoundErrorType) metricSecretReadErrorsTotal, _ = azureKVSecretReadErrorsTotal.GetMetricWithLabelValues(fakeKeyVaultName, fakeKeyVaultTenant, path, key, errors.BackendSecretNotFoundErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricSecretReadErrorsTotal)) azureKVSecretReadErrorsTotal.Reset() metrics.updateSecretReadErrorsTotalMetric(path, errors.BackendSecretForbiddenErrorType) metricSecretReadErrorsTotal, _ = azureKVSecretReadErrorsTotal.GetMetricWithLabelValues(fakeKeyVaultName, fakeKeyVaultTenant, path, key, errors.BackendSecretForbiddenErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricSecretReadErrorsTotal)) } ================================================ FILE: backend/azure_kv_test.go ================================================ package backend import ( "context" "encoding/json" "fmt" "net/http" "testing" "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" ) const ( fakeKeyVaultName = "azure-keyvault-fake-name" fakeKeyVaultTenant = "01234567-0123-0123-0123-0123456789ab" fakeKeyVaultSecret = "fake-secret" ) var akvSecrets = map[string]struct { value string access bool }{ fakeKeyVaultSecret: {value: "some-fake-value", access: true}, "exists": {value: "yes", access: true}, "internal-error": {value: "\"bad-scaped", access: true}, "forbidden": {value: "yes", access: false}, } func akvGetSecret(w http.ResponseWriter, r *http.Request) { // Info about what keyvault responses should look like extracted from // https://github.com/Azure/azure-sdk-for-go/blob/c73b114ded83c0a9c2685336b8b90836c1530cb3/sdk/keyvault/azsecrets/testdata/recordings/TestSetGetSecret.json vars := mux.Vars(r) jsonData := "{}" if v, ok := akvSecrets[vars["secretName"]]; ok { if v.access { jsonData = fmt.Sprintf(` { "value": "%s", "id": "https://%s.vault.azure.net/secrets/%s/3f3b11064811494a8a8b27edf4f0985b", "attributes": { "enabled": true, "created": 1643130727, "updated": 1643130727, "recoveryLevel": "CustomizedRecoverable\u002BPurgeable", "recoverableDays": 7 } }`, v.value, fakeKeyVaultName, vars["secretName"]) } else { w.WriteHeader(http.StatusForbidden) } } else { jsonData = fmt.Sprintf(` "error": { "code": "SecretNotFound", "message": "Secret not found: %s" } `, vars["secretName"]) w.WriteHeader(http.StatusNotFound) } var response interface{} if err := json.Unmarshal([]byte(jsonData), &response); err != nil { fmt.Printf("unable to unmarshal json %v", err) w.WriteHeader(http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") w.Header().Set("x-ms-keyvault-network-info", "conn_type=Ipv4;addr=72.49.29.93;act_addr_fam=InterNetwork;") w.Header().Set("x-ms-keyvault-region", "westus2") w.Header().Set("x-ms-keyvault-service-version", "1.9.264.2") w.Header().Set("x-ms-request-id", "868ba1d2-efe7-4930-b3ad-d010cf499778") w.Header().Set("X-Powered-By", "ASP.NET") // Trick Azure client to make it think everything is legit w.Header().Set( "WWW-Authenticate", "Bearer authorization=\u0022https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47\u0022, resource=\u0022https://vault.azure.net\u0022", ) json.NewEncoder(w).Encode(response) } // Copied from https://github.com/Azure/azure-sdk-for-go/blob/35fb64f82ef3b3308f55b1da37c1fec36bdd4166/sdk/keyvault/azsecrets/utils_test.go type FakeCredential struct { accountName string accountKey string } func NewFakeCredential(accountName, accountKey string) *FakeCredential { return &FakeCredential{ accountName: accountName, accountKey: accountKey, } } func (f *FakeCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (*azcore.AccessToken, error) { return &azcore.AccessToken{ Token: "faketoken", ExpiresOn: time.Date(2040, time.January, 1, 1, 1, 1, 1, time.UTC), }, nil } func TestGetAzureCredential(t *testing.T) { cases := []struct { cfg Config err bool typ azcore.TokenCredential msg string }{ { Config{}, true, nil, "Empty config should not be able to generate any client", }, { Config{AzureKVManagedClientID: "fake-client-id"}, false, new(azidentity.ManagedIdentityCredential), "Managed identity client should be generated using managed client ID", }, { Config{AzureKVManagedResourceID: "fake-resource-id"}, false, new(azidentity.ManagedIdentityCredential), "Managed identity client should be generated using managed resource ID", }, { Config{ AzureKVTenantID: "fake-tenant-id", }, true, nil, "Incomplete config should not generate any client (tenant)", }, { Config{ AzureKVClientID: "fake-client-id", }, true, nil, "Incomplete config should not generate any client (clientID)", }, { Config{ AzureKVClientSecret: "fake-client-secret", }, true, nil, "Incomplete config should not generate any client (clientID)", }, { Config{ AzureKVTenantID: "fake-tenant-id", AzureKVClientID: "fake-client-id", }, true, nil, "Incomplete config should not generate any client (tenant, clientID)", }, { Config{ AzureKVClientID: "fake-client-id", AzureKVClientSecret: "fake-client-secret", }, true, nil, "Incomplete config should not generate any client (clientID, clientSecret)", }, { Config{ AzureKVTenantID: "fake-tenant-id", AzureKVClientID: "fake-client-id", AzureKVClientSecret: "fake-client-secret", }, false, new(azidentity.ClientSecretCredential), "ClientSecretCredential should be generated with TenantID, ClientID and ClientSecret", }, } for _, c := range cases { client, err := getAzureCredential(context.TODO(), logger, c.cfg) if c.err { assert.NotNilf(t, err, c.msg) } else { assert.Nilf(t, err, c.msg) } if c.typ == nil { assert.Nilf(t, client, c.msg) } else { assert.IsTypef(t, c.typ, client, c.msg) } } } func TestAzureKeyVaultClient(t *testing.T) { cfg := Config{} client, err := azureKeyVaultClient(context.TODO(), logger, cfg) assert.NotNilf(t, err, "Empty config should generate an error") assert.Nilf(t, client, "Empty config should not generate any client") // Managed Identity auth is performed at client call, so the client generated is "valid" cfg = Config{ AzureKVTenantID: fakeKeyVaultTenant, AzureKVClientID: "fake-client-id", } client, err = azureKeyVaultClient(context.TODO(), logger, cfg) assert.NotNilf(t, err, "Invalid Service Principal Authentication should generate an error") assert.Nilf(t, client, "Invalid Service Principal Authentication should not generate any client") // Authentication is performed at client call, so the client generated is "valid" // This happens for both service principal and managed identity cfg = Config{ AzureKVTenantID: fakeKeyVaultTenant, AzureKVClientID: "fake-client-id", AzureKVClientSecret: "fake-client-secret", } client, err = azureKeyVaultClient(context.TODO(), logger, cfg) assert.Nilf(t, err, "Service Principal Authentication should not generate error") assert.NotNilf(t, client, "Service Principal Authentication should generate a client") cfg = Config{AzureKVManagedClientID: "fake-client-id"} client, err = azureKeyVaultClient(context.TODO(), logger, cfg) assert.Nilf(t, err, "Managed Identity Authentication should not generate error") assert.NotNilf(t, client, "Managed Identity Authentication should generate a client") } func TestAzureKVClientReadSecret(t *testing.T) { akvMetrics = newAzureKVMetrics(fakeKeyVaultName, fakeKeyVaultTenant) azClient, _ := azsecrets.NewClient( testingCfg.VaultURL, // Is a mock server, valid for both cases NewFakeCredential("fake", "fake"), nil, ) client := azureKVClient{ client: azClient, keyvaultName: "fakekvurl", context: context.TODO(), logger: logger, } value, err := client.ReadSecret("exists", "") assert.Nil(t, err) assert.Equal(t, akvSecrets["exists"].value, value) value, err = client.ReadSecret(fakeKeyVaultSecret, "") assert.Nil(t, err) assert.Equal(t, akvSecrets[fakeKeyVaultSecret].value, value) value, err = client.ReadSecret("forbidden", "") assert.NotNil(t, err) assert.Equal(t, "", value) assert.IsType(t, new(azcore.ResponseError), err) value, err = client.ReadSecret("not-found", "") assert.NotNil(t, err) assert.Equal(t, "", value) assert.IsType(t, new(azcore.ResponseError), err) value, err = client.ReadSecret("internal-error", "") assert.NotNil(t, err) assert.Equal(t, "", value) } ================================================ FILE: backend/backend.go ================================================ package backend import ( "context" "time" "github.com/go-logr/logr" "github.com/tuenti/secrets-manager/errors" ) const ( vaultBackendName = "vault" azureKVBackendName = "azure-kv" ) var supportedBackends map[string]bool func init() { supportedBackends = map[string]bool{ vaultBackendName: true, azureKVBackendName: true, } } // Config type represent backend config, and should include all backends config type Config struct { BackendTimeout time.Duration VaultURL string VaultAuthMethod string VaultRoleID string VaultSecretID string VaultKubernetesRole string VaultMaxTokenTTL int64 VaultTokenPollingPeriod time.Duration VaultRenewTTLIncrement int VaultEngine string VaultApprolePath string VaultKubernetesPath string AzureKVName string AzureKVTenantID string AzureKVClientID string AzureKVClientSecret string AzureKVManagedClientID string AzureKVManagedResourceID string } // Client interface represent a backend client interface that should be implemented type Client interface { ReadSecret(path string, key string) (string, error) } // NewBackendClient returns and implementation of Client interface, given the selected backend func NewBackendClient(ctx context.Context, backend string, logger logr.Logger, cfg Config) (*Client, error) { var err error var client Client if !supportedBackends[backend] { err = &errors.BackendNotImplementedError{ErrType: errors.BackendNotImplementedErrorType, Backend: backend} return nil, err } switch backend { case vaultBackendName: vclient, verr := vaultClient(logger, cfg) if verr != nil { return nil, verr } vclient.startTokenRenewer(ctx) client = vclient err = verr case azureKVBackendName: akvclient, akverr := azureKeyVaultClient(ctx, logger, cfg) if akverr != nil { return nil, akverr } client = akvclient err = akverr } return &client, err } ================================================ FILE: backend/backend_test.go ================================================ package backend import ( "context" "fmt" "net/http/httptest" "os" "sync" "testing" "github.com/go-logr/logr" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/tuenti/secrets-manager/errors" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) var ( testingCfg Config server *httptest.Server mutex sync.Mutex logger logr.Logger ) func TestNotImplementedBackend(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() cfg := Config{} backend := "foo" _, err := NewBackendClient(ctx, backend, nil, cfg) assert.EqualError(t, err, fmt.Sprintf("[%s] backend %s not supported", errors.BackendNotImplementedErrorType, backend)) } func TestMain(m *testing.M) { r := mux.NewRouter() v1SysHandler := r.PathPrefix(fmt.Sprintf("/%s/sys", vaultAPIVersion)).Subrouter() v1AuthHandler := r.PathPrefix(fmt.Sprintf("/%s/auth", vaultAPIVersion)).Subrouter() v1SecretHandler := r.PathPrefix(fmt.Sprintf("/%s/secret", vaultAPIVersion)).Subrouter() akvSecretsHandler := r.PathPrefix("/secrets").Subrouter() v1SysHandler.HandleFunc("/health", v1SysHealth).Methods("GET") v1AuthHandler.HandleFunc("/token/lookup-self", v1AuthTokenLookupSelf).Methods("GET") v1AuthHandler.HandleFunc("/token/renew-self", v1AuthTokenRenewSelf).Methods("PUT") v1AuthHandler.HandleFunc("/approle/login", v1AuthAppRoleLogin).Methods("PUT") v1AuthHandler.HandleFunc("/kubernetes/login", v1AuthKubernetesLogin).Methods("PUT") v1SecretHandler.HandleFunc("/data/test", v1SecretTestKv2).Methods("GET") v1SecretHandler.HandleFunc("/test", v1SecretTestKv1).Methods("GET") akvSecretsHandler.PathPrefix("/{secretName}").HandlerFunc(akvGetSecret).Methods("GET") server = httptest.NewServer(r) defer server.Close() testingCfg = Config{ VaultURL: string(server.URL), VaultRoleID: vaultFakeRoleID, VaultSecretID: vaultFakeSecretID, VaultTokenPollingPeriod: 1, VaultEngine: "kv2", VaultApprolePath: vaultAppRolePath, } vaultTestCfg = &testConfig{ tokenRenewable: defaultTokenRenewable, tokenTTL: defaultTokenTTL, tokenRevoked: defaultRevokedToken, invalidRoleID: defaultInvalidAppRole, invalidSecretID: defaultInvalidAppRole, } logger = zap.New(zap.UseDevMode(true)) os.Exit(m.Run()) } ================================================ FILE: backend/decoder.go ================================================ package backend import ( "encoding/base64" "github.com/tuenti/secrets-manager/errors" ) const ( // Base64EncodingType is the internal code to represent a base64 encoding Base64EncodingType = "base64" // TextEncodingType is the internal code to represent a basic text encoding TextEncodingType = "text" // DefaultEncodingType is the default encoding to use. DefaultEncodingType = "text" ) // Decoder interface represents anything that can implement DecodeString: get some bytes from input string type Decoder interface { DecodeString(input string) ([]byte, error) } // Base64Decoder represents a Decoder for base64 text type Base64Decoder struct { Encoding string } // TextDecoder represents a Decoder for plain text type TextDecoder struct { Encoding string } // DecodeString for Base64Decoder will get the text version (in bytes) of the input base64 text func (d Base64Decoder) DecodeString(input string) ([]byte, error) { data, err := base64.StdEncoding.DecodeString(input) if err != nil { return nil, err } return data, err } // DecodeString for TextDecoder, will simply cast to []bytes the input text func (d TextDecoder) DecodeString(input string) ([]byte, error) { return []byte(input), nil } // NewDecoder returns a new Decoder implementation or an error if the provided encoding is not implemented func NewDecoder(encoding string) (Decoder, error) { if encoding == "" { encoding = DefaultEncodingType } switch encoding { case Base64EncodingType: return Base64Decoder{Encoding: encoding}, nil case TextEncodingType: return TextDecoder{Encoding: encoding}, nil default: return nil, &errors.EncodingNotImplementedError{ErrType: errors.EncodingNotImplementedErrorType, Encoding: encoding} } } ================================================ FILE: backend/decoder_test.go ================================================ package backend import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/tuenti/secrets-manager/errors" ) func TestNotImplementedDecoder(t *testing.T) { encoding := "foo" _, err := NewDecoder(encoding) assert.EqualError(t, err, fmt.Sprintf("[%s] encoding %s not supported", errors.EncodingNotImplementedErrorType, encoding)) } func TestGetB64Decoder(t *testing.T) { encoding := "base64" decoder, err := NewDecoder(encoding) b64Decoder := decoder.(Base64Decoder) assert.Nil(t, err) assert.Equal(t, encoding, b64Decoder.Encoding) } func TestGetTextDecoder(t *testing.T) { encoding := "text" decoder, err := NewDecoder(encoding) textDecoder := decoder.(TextDecoder) assert.Nil(t, err) assert.Equal(t, encoding, textDecoder.Encoding) } func TestGetTextDecoderFromEmptyString(t *testing.T) { encoding := "" decoder, err := NewDecoder(encoding) textDecoder := decoder.(TextDecoder) assert.Nil(t, err) assert.Equal(t, "text", textDecoder.Encoding) } func TestDecodeB64String(t *testing.T) { b64data := "dGVzdGluZyBiYXNlNjQgZGVjb2Rpbmc=" decoder, _ := NewDecoder("base64") data, err := decoder.DecodeString(b64data) assert.Nil(t, err) assert.Equal(t, "testing base64 decoding", fmt.Sprintf("%s", data)) } func TestDecodeInvalidB64String(t *testing.T) { b64data := "Invalid b64 data" decoder, _ := NewDecoder("base64") data, err := decoder.DecodeString(b64data) assert.NotNil(t, err) assert.Nil(t, data) } func TestDecodeText(t *testing.T) { text := "secret text" decoder, _ := NewDecoder("text") data, err := decoder.DecodeString(text) assert.Nil(t, err) assert.Equal(t, text, fmt.Sprintf("%s", data)) } ================================================ FILE: backend/vault.go ================================================ package backend import ( "context" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "os" "strconv" "time" "github.com/go-logr/logr" "github.com/hashicorp/vault/api" "github.com/tuenti/secrets-manager/errors" ) var vMetrics *vaultMetrics const ( defaultSecretKey = "data" kubernetesJwtTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" kubernetesAuthMethod = "kubernetes" appRoleAuthMethod = "approle" ) type client struct { vclient *api.Client logical *api.Logical roleID string authMethod string secretID string kubernetesRole string maxTokenTTL int64 tokenPollingPeriod time.Duration renewTTLIncrement int engine engine approlePath string kubernetesPath string logger logr.Logger } func (c *client) vaultLogin() error { switch c.authMethod { case kubernetesAuthMethod: fd, err := os.Open(kubernetesJwtTokenPath) defer fd.Close() if err != nil { return err } return c.vaultKubernetesLogin(fd) case appRoleAuthMethod: fallthrough default: return c.vaultAppRoleLogin() } } func (c *client) vaultAppRoleLogin() error { appRole := map[string]interface{}{ "role_id": c.roleID, "secret_id": c.secretID, } resp, err := c.logical.Write(fmt.Sprintf("auth/%s/login", c.approlePath), appRole) if err != nil { return err } c.vclient.SetToken(resp.Auth.ClientToken) return nil } func (c *client) vaultKubernetesLogin(podSATokenReader io.Reader) error { jwt, err := ioutil.ReadAll(podSATokenReader) if err != nil { return err } kubernetes := map[string]interface{}{ "jwt": string(jwt), "role": c.kubernetesRole, } resp, err := c.logical.Write(fmt.Sprintf("auth/%s/login", c.kubernetesPath), kubernetes) if err != nil { return err } c.vclient.SetToken(resp.Auth.ClientToken) return nil } func vaultClient(l logr.Logger, cfg Config) (*client, error) { logger := l.WithName("vault").WithValues( "vault_url", cfg.VaultURL, "vault_engine", cfg.VaultEngine) httpClient := new(http.Client) httpClient.Timeout = cfg.BackendTimeout vclient, err := api.NewClient(&api.Config{Address: cfg.VaultURL, HttpClient: httpClient}) if err != nil { logger.Error(err, "unable to create vault api client") return nil, err } logical := vclient.Logical() engine, err := newEngine(cfg.VaultEngine) if err != nil { logger.Error(err, "unable to setup vault engine") return nil, err } client := client{ vclient: vclient, logical: logical, authMethod: cfg.VaultAuthMethod, roleID: cfg.VaultRoleID, secretID: cfg.VaultSecretID, kubernetesRole: cfg.VaultKubernetesRole, maxTokenTTL: cfg.VaultMaxTokenTTL, tokenPollingPeriod: cfg.VaultTokenPollingPeriod, renewTTLIncrement: cfg.VaultRenewTTLIncrement, engine: engine, approlePath: cfg.VaultApprolePath, kubernetesPath: cfg.VaultKubernetesPath, } err = client.vaultLogin() if err != nil { logger.Error(err, "unable to login to vault with provided credentials") return nil, err } sys := vclient.Sys() health, err := sys.Health() if err != nil { logger.Error(err, "could not get health information about vault cluster") return nil, err } logger = logger.WithValues( "vault_cluster_name", health.ClusterName, "vault_cluster_id", health.ClusterID, "vault_version", health.Version, "vault_sealed", strconv.FormatBool(health.Sealed), "vault_server_time_utc", health.ServerTimeUTC, ) logger.Info("successfully logged into vault cluster") client.logger = logger vMetrics = newVaultMetrics(cfg.VaultURL, health.Version, cfg.VaultEngine, health.ClusterID, health.ClusterName) vMetrics.updateVaultMaxTokenTTLMetric(cfg.VaultMaxTokenTTL) return &client, err } func (c *client) getToken() (*api.Secret, error) { auth := c.vclient.Auth() lookup, err := auth.Token().LookupSelf() if err != nil { vMetrics.updateVaultTokenRenewalErrorsTotalMetric(vaultLookupSelfOperationName, errors.UnknownErrorType) return nil, err } return lookup, nil } func (c *client) getTokenTTL(token *api.Secret) (int64, error) { var ttl int64 ttl, err := token.Data["ttl"].(json.Number).Int64() if err != nil { return -1, err } vMetrics.updateVaultTokenTTLMetric(ttl) return ttl, nil } func (c *client) renewToken(token *api.Secret) error { isRenewable, err := token.TokenIsRenewable() if err != nil { vMetrics.updateVaultTokenRenewalErrorsTotalMetric(vaultIsRenewableOperationName, errors.UnknownErrorType) return err } if !isRenewable { vMetrics.updateVaultTokenRenewalErrorsTotalMetric(vaultIsRenewableOperationName, errors.VaultTokenNotRenewableErrorType) err = &errors.VaultTokenNotRenewableError{ErrType: errors.VaultTokenNotRenewableErrorType} return err } auth := c.vclient.Auth() if _, err = auth.Token().RenewSelf(c.renewTTLIncrement); err != nil { vMetrics.updateVaultTokenRenewalErrorsTotalMetric(vaultRenewSelfOperationName, errors.UnknownErrorType) return err } return nil } func (c *client) renewalLoop() { token, err := c.getToken() if err != nil { c.logger.Error(err, "unable to get vault token") c.logger.Info("trying to login to vault again") if err = c.vaultLogin(); err != nil { vMetrics.updateVaultLoginErrorsTotalMetric() c.logger.Error(err, "login error, vault token not obtained") } else { c.logger.Info("login successful, got a new vault token") } return } ttl, err := c.getTokenTTL(token) if err != nil { c.logger.Error(err, "failed to read vault token TTL") } else if ttl < c.maxTokenTTL { c.logger.Info("vault token is really close to expire", "vault_token_ttl", ttl) err := c.renewToken(token) if err != nil { c.logger.Error(err, "failed to renew vault token") } else { c.logger.Info("vault token renewed successfully!") } } return } func (c *client) startTokenRenewer(ctx context.Context) { go func(ctx context.Context) { for { select { case <-time.After(c.tokenPollingPeriod): c.renewalLoop() break case <-ctx.Done(): c.logger.Info("gracefully shutting down token renewal go routine") return } } }(ctx) } func (c *client) ReadSecret(path string, key string) (string, error) { data := "" if key == "" { key = defaultSecretKey } logical := c.logical secret, err := logical.Read(path) if err != nil { vMetrics.updateVaultSecretReadErrorsTotalMetric(path, key, errors.UnknownErrorType) return data, err } if secret != nil { secretData := c.engine.getData(secret) warnings := secret.Warnings if secretData != nil { if secretData[key] != nil { data = secretData[key].(string) } else { vMetrics.updateVaultSecretReadErrorsTotalMetric(path, key, errors.BackendSecretNotFoundErrorType) err = &errors.BackendSecretNotFoundError{ErrType: errors.BackendSecretNotFoundErrorType, Path: path, Key: key} } } else { for _, w := range warnings { c.logger.Info("secret contains warnings", "vault_secret_warning", w) } vMetrics.updateVaultSecretReadErrorsTotalMetric(path, key, errors.BackendSecretNotFoundErrorType) err = &errors.BackendSecretNotFoundError{ErrType: errors.BackendSecretNotFoundErrorType, Path: path, Key: key} } } else { vMetrics.updateVaultSecretReadErrorsTotalMetric(path, key, errors.BackendSecretNotFoundErrorType) err = &errors.BackendSecretNotFoundError{ErrType: errors.BackendSecretNotFoundErrorType, Path: path, Key: key} } return data, err } ================================================ FILE: backend/vault_engine.go ================================================ package backend import ( "github.com/hashicorp/vault/api" "github.com/tuenti/secrets-manager/errors" ) const ( kvEngineV1Name = "kv1" kvEngineV2Name = "kv2" ) type engine interface { getData(s *api.Secret) map[string]interface{} } type kvEngineV1 struct { name string } type kvEngineV2 struct { name string } func (e kvEngineV1) getData(s *api.Secret) map[string]interface{} { return s.Data } func (e kvEngineV2) getData(s *api.Secret) map[string]interface{} { if s.Data["data"] == nil { return nil } return s.Data["data"].(map[string]interface{}) } func newEngine(eng string) (engine, error) { if eng == "" { eng = kvEngineV2Name } switch eng { case kvEngineV1Name: return kvEngineV1{name: kvEngineV1Name}, nil case kvEngineV2Name: return kvEngineV2{name: kvEngineV2Name}, nil default: return nil, &errors.VaultEngineNotImplementedError{ErrType: errors.VaultEngineNotImplementedErrorType, Engine: eng} } } ================================================ FILE: backend/vault_engine_test.go ================================================ package backend import ( "fmt" "testing" "github.com/hashicorp/vault/api" "github.com/stretchr/testify/assert" "github.com/tuenti/secrets-manager/errors" ) func TestNewEngineKV1(t *testing.T) { eng := "kv1" engine, err := newEngine(eng) assert.Nil(t, err) assert.Equal(t, eng, engine.(kvEngineV1).name) } func TestNewEngineKV2(t *testing.T) { eng := "kv2" engine, err := newEngine(eng) assert.Nil(t, err) assert.Equal(t, eng, engine.(kvEngineV2).name) } func TestNotImplementedEngine(t *testing.T) { eng := "kv3" _, err := newEngine(eng) assert.NotNil(t, err) assert.EqualError(t, err, fmt.Sprintf("[%s] vault engine %s not supported", errors.VaultEngineNotImplementedErrorType, eng)) } func TestGetDataKv1(t *testing.T) { data := make(map[string]interface{}) data["foo"] = "bar" s := &api.Secret{Data: data} engine, _ := newEngine("kv1") d := engine.getData(s) assert.NotNil(t, d) assert.Equal(t, data, d) } func TestGetDataKv2(t *testing.T) { data := make(map[string]interface{}) nested := make(map[string]interface{}) nested["foo"] = "bar" data["data"] = nested s := &api.Secret{Data: data} engine, _ := newEngine("kv2") d := engine.getData(s) assert.NotNil(t, d) assert.Equal(t, nested, d) } func TestGetDataKv2WithKv1Engine(t *testing.T) { data := make(map[string]interface{}) nested := make(map[string]interface{}) nested["foo"] = "bar" data["data"] = nested s := &api.Secret{Data: data} engine, _ := newEngine("kv1") d := engine.getData(s) assert.NotNil(t, d) assert.Equal(t, data, d) } ================================================ FILE: backend/vault_metrics.go ================================================ package backend import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "sigs.k8s.io/controller-runtime/pkg/metrics" ) const ( vaultLookupSelfOperationName = "lookup-self" vaultRenewSelfOperationName = "renew-self" vaultIsRenewableOperationName = "is-renewable" ) var ( vaultLabelNames = []string{"vault_address", "vault_engine", "vault_version", "vault_cluster_id", "vault_cluster_name"} secretLabelNames = []string{"path", "key", "error"} vaultErrorLabelNames = []string{"vault_operation", "error"} // Prometeheus metrics: https://prometheus.io tokenTTL = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "secrets_manager", Subsystem: "vault", Name: "token_ttl", Help: "Vault token TTL", }, vaultLabelNames) maxTokenTTL = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "secrets_manager", Subsystem: "vault", Name: "max_token_ttl", Help: "secrets-manager max Vault token TTL", }, vaultLabelNames) tokenRenewalErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: "secrets_manager", Subsystem: "vault", Name: "token_renewal_errors_total", Help: "Vault token renewal errors counter", }, append(vaultLabelNames, vaultErrorLabelNames...)) secretReadErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: "secrets_manager", Subsystem: "vault", Name: "read_secret_errors_total", Help: "Vault read operations counter", }, append(vaultLabelNames, secretLabelNames...)) loginErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: "secrets_manager", Subsystem: "vault", Name: "login_errors_total", Help: "Vault login errors counter", }, vaultLabelNames) ) type vaultMetrics struct { vaultLabels map[string]string } func init() { r := metrics.Registry r.MustRegister(tokenTTL) r.MustRegister(maxTokenTTL) r.MustRegister(tokenRenewalErrorsTotal) r.MustRegister(secretReadErrorsTotal) r.MustRegister(loginErrorsTotal) } func newVaultMetrics(vaultAddr string, vaultVersion string, vaultEngine string, vaultClusterID string, vaultClusterName string) *vaultMetrics { labels := make(map[string]string, len(vaultLabelNames)) labels["vault_addr"] = vaultAddr labels["vault_engine"] = vaultEngine labels["vault_version"] = vaultVersion labels["vault_cluster_id"] = vaultClusterID labels["vault_cluster_name"] = vaultClusterName return &vaultMetrics{vaultLabels: labels} } func (vm *vaultMetrics) updateVaultMaxTokenTTLMetric(value int64) { maxTokenTTL.WithLabelValues( vm.vaultLabels["vault_addr"], vm.vaultLabels["vault_engine"], vm.vaultLabels["vault_version"], vm.vaultLabels["vault_cluster_id"], vm.vaultLabels["vault_cluster_name"]).Set(float64(value)) } func (vm *vaultMetrics) updateVaultTokenTTLMetric(value int64) { tokenTTL.WithLabelValues( vm.vaultLabels["vault_addr"], vm.vaultLabels["vault_engine"], vm.vaultLabels["vault_version"], vm.vaultLabels["vault_cluster_id"], vm.vaultLabels["vault_cluster_name"]).Set(float64(value)) } func (vm *vaultMetrics) updateVaultSecretReadErrorsTotalMetric(path string, key string, errorType string) { secretReadErrorsTotal.WithLabelValues( vm.vaultLabels["vault_addr"], vm.vaultLabels["vault_engine"], vm.vaultLabels["vault_version"], vm.vaultLabels["vault_cluster_id"], vm.vaultLabels["vault_cluster_name"], path, key, errorType).Inc() } func (vm *vaultMetrics) updateVaultTokenRenewalErrorsTotalMetric(vaultOperation string, errorType string) { tokenRenewalErrorsTotal.WithLabelValues( vm.vaultLabels["vault_addr"], vm.vaultLabels["vault_engine"], vm.vaultLabels["vault_version"], vm.vaultLabels["vault_cluster_id"], vm.vaultLabels["vault_cluster_name"], vaultOperation, errorType).Inc() } func (vm *vaultMetrics) updateVaultLoginErrorsTotalMetric() { loginErrorsTotal.WithLabelValues( vm.vaultLabels["vault_addr"], vm.vaultLabels["vault_engine"], vm.vaultLabels["vault_version"], vm.vaultLabels["vault_cluster_id"], vm.vaultLabels["vault_cluster_name"]).Inc() } ================================================ FILE: backend/vault_metrics_test.go ================================================ package backend import ( "testing" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/tuenti/secrets-manager/errors" ) const ( fakeVaultAddress = "https://vault.example.com:8200" fakeVaultVersion = "0.11.1" fakeVaultEngine = "kv2" fakeVaultClusterID = "vault-fake-1" fakeVaultClusterName = "vault-fake" ) func TestUpdateMaxTokenTTL(t *testing.T) { metrics := newVaultMetrics(fakeVaultAddress, fakeVaultVersion, fakeVaultEngine, fakeVaultClusterID, fakeVaultClusterName) maxTokenTTL.Reset() metrics.updateVaultMaxTokenTTLMetric(600) metricMaxTokenTTL, _ := maxTokenTTL.GetMetricWithLabelValues(fakeVaultAddress, fakeVaultEngine, fakeVaultVersion, fakeVaultClusterID, fakeVaultClusterName) assert.Equal(t, 600.0, testutil.ToFloat64(metricMaxTokenTTL)) } func TestUpdateTokenTTL(t *testing.T) { metrics := newVaultMetrics(fakeVaultAddress, fakeVaultVersion, fakeVaultEngine, fakeVaultClusterID, fakeVaultClusterName) tokenTTL.Reset() metrics.updateVaultTokenTTLMetric(300) metricTokenTTL, _ := tokenTTL.GetMetricWithLabelValues(fakeVaultAddress, fakeVaultEngine, fakeVaultVersion, fakeVaultClusterID, fakeVaultClusterName) assert.Equal(t, 300.0, testutil.ToFloat64(metricTokenTTL)) } func TestUpdateTokenLookupErrorsTotal(t *testing.T) { metrics := newVaultMetrics(fakeVaultAddress, fakeVaultVersion, fakeVaultEngine, fakeVaultClusterID, fakeVaultClusterName) tokenRenewalErrorsTotal.Reset() metrics.updateVaultTokenRenewalErrorsTotalMetric(vaultLookupSelfOperationName, errors.UnknownErrorType) metricTokenRenewalErrorsTotal, _ := tokenRenewalErrorsTotal.GetMetricWithLabelValues(fakeVaultAddress, fakeVaultEngine, fakeVaultVersion, fakeVaultClusterID, fakeVaultClusterName, vaultLookupSelfOperationName, errors.UnknownErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricTokenRenewalErrorsTotal)) } func TestUpdateTokenRenewErrorsTotal(t *testing.T) { metrics := newVaultMetrics(fakeVaultAddress, fakeVaultVersion, fakeVaultEngine, fakeVaultClusterID, fakeVaultClusterName) tokenRenewalErrorsTotal.Reset() metrics.updateVaultTokenRenewalErrorsTotalMetric(vaultRenewSelfOperationName, errors.UnknownErrorType) metricTokenRenewalErrorsTotal, _ := tokenRenewalErrorsTotal.GetMetricWithLabelValues(fakeVaultAddress, fakeVaultEngine, fakeVaultVersion, fakeVaultClusterID, fakeVaultClusterName, vaultRenewSelfOperationName, errors.UnknownErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricTokenRenewalErrorsTotal)) tokenRenewalErrorsTotal.Reset() metrics.updateVaultTokenRenewalErrorsTotalMetric(vaultIsRenewableOperationName, errors.VaultTokenNotRenewableErrorType) metricTokenRenewalErrorsTotal, _ = tokenRenewalErrorsTotal.GetMetricWithLabelValues(fakeVaultAddress, fakeVaultEngine, fakeVaultVersion, fakeVaultClusterID, fakeVaultClusterName, vaultIsRenewableOperationName, errors.VaultTokenNotRenewableErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricTokenRenewalErrorsTotal)) } func TestUpdateReadSecretErrorsTotal(t *testing.T) { path := "/path/to/secret" key := "key" metrics := newVaultMetrics(fakeVaultAddress, fakeVaultVersion, fakeVaultEngine, fakeVaultClusterID, fakeVaultClusterName) secretReadErrorsTotal.Reset() metrics.updateVaultSecretReadErrorsTotalMetric(path, key, errors.UnknownErrorType) metricSecretReadErrorsTotal, _ := secretReadErrorsTotal.GetMetricWithLabelValues(fakeVaultAddress, fakeVaultEngine, fakeVaultVersion, fakeVaultClusterID, fakeVaultClusterName, path, key, errors.UnknownErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricSecretReadErrorsTotal)) secretReadErrorsTotal.Reset() metrics.updateVaultSecretReadErrorsTotalMetric(path, key, errors.BackendSecretNotFoundErrorType) metricSecretReadErrorsTotal, _ = secretReadErrorsTotal.GetMetricWithLabelValues(fakeVaultAddress, fakeVaultEngine, fakeVaultVersion, fakeVaultClusterID, fakeVaultClusterName, path, key, errors.BackendSecretNotFoundErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricSecretReadErrorsTotal)) } ================================================ FILE: backend/vault_test.go ================================================ package backend import ( "context" "encoding/json" "fmt" "net/http" "strings" "testing" "time" "github.com/hashicorp/vault/api" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/tuenti/secrets-manager/errors" ) const ( vaultAPIVersion = "v1" vaultFakeClusterName = "vault-mock-cluster" vaultFakeClusterID = "vault-mock-cluster-1" vaultFakeVersion = "0.11.1" selectedBackend = "vault" fakeToken = "fake-token" vaultFakeRoleID = "12345678-9aaa-bbbb-cccc-dddddddddddd" vaultFakeSecretID = "eeeeeeee-ffff-0000-1111-123456789aaa" vaultAppRolePath = "approle" defaultTokenTTL = 40 defaultTokenRenewable = true defaultRevokedToken = false defaultInvalidAppRole = false defaultKubernetesRole = false fakeKubernetesSAToken = `eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.bQTnz6AuMJvmXXQsVPrxeQNvzDkimo7VNXxHeSBfClLufmCVZRUuyTwJF311JHuh` ) type testConfig struct { tokenTTL int tokenRenewable bool tokenRevoked bool invalidRoleID bool invalidSecretID bool invalidKubernetesRole bool } var ( vaultTestCfg *testConfig ) func v1SysHealth(w http.ResponseWriter, r *http.Request) { var response interface{} jsonData := fmt.Sprintf(` { "initialized": true, "sealed": false, "standby": false, "performance_standby": false, "replication_performance_mode": "disabled", "replication_dr_mode": "disabled", "server_time_utc": 1537804485, "version": "%s", "cluster_name": "%s", "cluster_id": "%s" }`, vaultFakeVersion, vaultFakeClusterName, vaultFakeClusterID) if err := json.Unmarshal([]byte(jsonData), &response); err != nil { fmt.Printf("unable to unmarshal json %v", err) w.WriteHeader(http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func v1AuthTokenLookupSelf(w http.ResponseWriter, r *http.Request) { var response interface{} jsonData := "" if !vaultTestCfg.tokenRevoked { jsonData = fmt.Sprintf(` { "request_id": "8d70f864-5f77-44fe-0940-df085376101f", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "accessor": "d2d7308c-b9f2-3399-4202-11d670b8c053", "creation_time": 1537810558, "creation_ttl": 60, "display_name": "token", "entity_id": "", "expire_time": "2018-09-24T17:36:58.797772932Z", "explicit_max_ttl": 0, "id": "31a5ea4e-907d-c1b9-1dfc-6b88526be248", "issue_time": "2018-09-24T17:35:58.79776585Z", "meta": null, "num_uses": 0, "orphan": false, "path": "auth/token/create", "policies": [ "fake-policy" ], "renewable": %t, "ttl": %d }, "wrap_info": null, "warnings": null, "auth": null }`, vaultTestCfg.tokenRenewable, vaultTestCfg.tokenTTL) } else { jsonData = `{"errors":["permission denied"]}` w.WriteHeader(http.StatusForbidden) } if err := json.Unmarshal([]byte(jsonData), &response); err != nil { fmt.Printf("unable to unmarshal json %v", err) w.WriteHeader(http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func v1AuthTokenRenewSelf(w http.ResponseWriter, r *http.Request) { var response interface{} jsonData := "" if !vaultTestCfg.tokenRevoked { jsonData = fmt.Sprintf(` { "request_id": "d8ae3e67-91a0-2f7a-528b-522048f9dad3", "lease_id": "", "renewable": false, "lease_duration": 0, "data": null, "wrap_info": null, "warnings": null, "auth": { "client_token": "%s", "accessor": "dc6aa861-3020-322c-8df5-4b08afa43a34", "policies": [ "fake-policy" ], "token_policies": [ "fake-policy" ], "metadata": null, "lease_duration": 1000, "renewable": true, "entity_id": "" } }`, fakeToken) } else { jsonData = `{"errors":["permission denied"]}` w.WriteHeader(http.StatusForbidden) } if err := json.Unmarshal([]byte(jsonData), &response); err != nil { fmt.Printf("unable to unmarshal json %v", err) w.WriteHeader(http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func v1AuthKubernetesLogin(w http.ResponseWriter, r *http.Request) { var response interface{} jsonData := "" if !vaultTestCfg.invalidKubernetesRole { jsonData = fmt.Sprintf(` { "auth": { "client_token": "%s", "accessor": "78e87a38-84ed-2692-538f-ca8b9f400ab3", "policies": ["secrets-manager"], "metadata": { "role": "secrets-manager", "service_account_name": "secrets-manager", "service_account_namespace": "default", "service_account_secret_name": "secrets-manager-token-pd21c", "service_account_uid": "aa9aa8ff-98d0-11e7-9bb7-0800276d99bf" }, "lease_duration": 2764800, "renewable": true } }`, fakeToken) } else { jsonData = `{"errors":["forbidden"]}` w.WriteHeader(http.StatusForbidden) } if err := json.Unmarshal([]byte(jsonData), &response); err != nil { fmt.Printf("unable to unmarshal json %v", err) w.WriteHeader(http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func v1AuthAppRoleLogin(w http.ResponseWriter, r *http.Request) { var response interface{} jsonData := "" if !vaultTestCfg.invalidRoleID && !vaultTestCfg.invalidSecretID { jsonData = fmt.Sprintf(` { "request_id": "ecc0025f-040a-3c28-164e-0651abd7f6ac", "lease_id": "", "renewable": false, "lease_duration": 0, "data": null, "wrap_info": null, "warnings": null, "auth": { "client_token": "%s", "accessor": "AEuaibYaTmrB44ZG6QjRpv0o", "policies": [ "default", "secrets-manager" ], "token_policies": [ "default", "secrets-manager" ], "metadata": { "role_name": "secrets-manager" }, "lease_duration": 1200, "renewable": true, "entity_id": "79619c25-955d-2888-7abf-52bf4b87ae94", "token_type": "service", "orphan": true } }`, fakeToken) } else if vaultTestCfg.invalidRoleID { jsonData = `{"errors":["invalid role ID"]}` w.WriteHeader(http.StatusBadRequest) } else { jsonData = `{"errors":["invalid secret ID"]}` w.WriteHeader(http.StatusBadRequest) } if err := json.Unmarshal([]byte(jsonData), &response); err != nil { fmt.Printf("unable to unmarshal json %v", err) w.WriteHeader(http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func v1SecretTestKv2(w http.ResponseWriter, r *http.Request) { var response interface{} jsonData := ` { "request_id": "a21f835e-7e72-dd43-d5a1-80fea23c0649", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "data": { "foo": "bar" }, "metadata": { "created_time": "2018-09-25T08:35:15.504392904Z", "deletion_time": "", "destroyed": false, "version": 1 } }, "wrap_info": null, "warnings": null, "auth": null }` if err := json.Unmarshal([]byte(jsonData), &response); err != nil { fmt.Printf("unable to unmarshal json %v", err) w.WriteHeader(http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func v1SecretTestKv1(w http.ResponseWriter, r *http.Request) { var response interface{} jsonData := ` { "request_id": "a21f835e-7e72-dd43-d5a1-80fea23c0649", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "foo": "bar" }, "wrap_info": null, "warnings": null, "auth": null }` if err := json.Unmarshal([]byte(jsonData), &response); err != nil { fmt.Printf("unable to unmarshal json %v", err) w.WriteHeader(http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func TestVaultLoginKubernetes(t *testing.T) { httpClient := new(http.Client) vclient, _ := api.NewClient(&api.Config{Address: testingCfg.VaultURL, HttpClient: httpClient}) c := &client{ vclient: vclient, logical: vclient.Logical(), authMethod: "kubernetes", kubernetesRole: "secrets-manager", kubernetesPath: "kubernetes", } err := c.vaultKubernetesLogin(strings.NewReader(fakeKubernetesSAToken)) assert.Nil(t, err) mutex.Lock() defer mutex.Unlock() vaultTestCfg.invalidKubernetesRole = true err2 := c.vaultKubernetesLogin(strings.NewReader(fakeKubernetesSAToken)) assert.NotNil(t, err2) } func TestVaultBackendInvalidCfg(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() cfg := Config{VaultURL: "http://1.1.1.1:8300", VaultEngine: "kv3", BackendTimeout: 1} backend := "vault" client, err := NewBackendClient(ctx, backend, logger, cfg) assert.NotNil(t, err) assert.Nil(t, client) } func TestVaultBackend(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() client, err := NewBackendClient(ctx, "vault", logger, testingCfg) assert.Nil(t, err) assert.NotNil(t, client) } func TestVaultLoginInvalidRoleId(t *testing.T) { mutex.Lock() defer mutex.Unlock() vaultTestCfg.invalidRoleID = true client, err := vaultClient(logger, testingCfg) assert.Nil(t, client) assert.NotNil(t, err) vaultTestCfg.invalidRoleID = defaultInvalidAppRole } func TestVaultLoginInvalidSecretId(t *testing.T) { mutex.Lock() defer mutex.Unlock() vaultTestCfg.invalidSecretID = true client, err := vaultClient(logger, testingCfg) assert.Nil(t, client) assert.NotNil(t, err) vaultTestCfg.invalidSecretID = defaultInvalidAppRole } func TestVaultClient(t *testing.T) { maxTokenTTL.Reset() client, err := vaultClient(logger, testingCfg) metricMaxTokenTTL, _ := maxTokenTTL.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName) assert.Nil(t, err) assert.NotNil(t, client) assert.Equal(t, float64(client.maxTokenTTL), testutil.ToFloat64(metricMaxTokenTTL)) } func TestVaultClientInvalidCfg(t *testing.T) { invalidCfg := Config{VaultURL: "http://1.1.1.1:8300", VaultRoleID: vaultFakeRoleID, VaultSecretID: vaultFakeSecretID, BackendTimeout: 1 * time.Second} client, err := vaultClient(logger, invalidCfg) assert.NotNil(t, err) assert.Nil(t, client) } func TestGetToken(t *testing.T) { client, _ := vaultClient(logger, testingCfg) token, err := client.getToken() assert.NotNil(t, token) assert.Nil(t, err) } func TestGetTokenTTL(t *testing.T) { client, _ := vaultClient(logger, testingCfg) tokenTTL.Reset() token, _ := client.getToken() ttl, err := client.getTokenTTL(token) metricTokenTTL, _ := tokenTTL.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName) assert.Equal(t, float64(vaultTestCfg.tokenTTL), testutil.ToFloat64(metricTokenTTL)) assert.Equal(t, int64(vaultTestCfg.tokenTTL), ttl) assert.Nil(t, err) } func TestRenewToken(t *testing.T) { client, _ := vaultClient(logger, testingCfg) mutex.Lock() defer mutex.Unlock() vaultTestCfg.tokenRenewable = true vaultTestCfg.tokenTTL = 600 client.maxTokenTTL = 6000 token, _ := client.getToken() err := client.renewToken(token) assert.Nil(t, err) } func TestRenewTokenRevokedToken(t *testing.T) { client, _ := vaultClient(logger, testingCfg) mutex.Lock() defer mutex.Unlock() vaultTestCfg.tokenRenewable = true vaultTestCfg.tokenTTL = 600 client.maxTokenTTL = 6000 token, _ := client.getToken() vaultTestCfg.tokenRevoked = true tokenRenewalErrorsTotal.Reset() err := client.renewToken(token) metricTokenRenewalErrorsTotal, _ := tokenRenewalErrorsTotal.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName, vaultRenewSelfOperationName, errors.UnknownErrorType) assert.NotNil(t, err) assert.Equal(t, 1.0, testutil.ToFloat64(metricTokenRenewalErrorsTotal)) } func TestTokenNotRenewableError(t *testing.T) { client, _ := vaultClient(logger, testingCfg) mutex.Lock() defer mutex.Unlock() vaultTestCfg.tokenRenewable = false vaultTestCfg.tokenTTL = 600 client.maxTokenTTL = 6000 token, _ := client.getToken() tokenRenewalErrorsTotal.Reset() err := client.renewToken(token) metricTokenRenewalErrorsTotal, _ := tokenRenewalErrorsTotal.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName, vaultIsRenewableOperationName, errors.VaultTokenNotRenewableErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricTokenRenewalErrorsTotal)) assert.EqualError(t, err, fmt.Sprintf("[%s] vault token not renewable", errors.VaultTokenNotRenewableErrorType)) } func TestRenewalLoopRevokedToken(t *testing.T) { client, _ := vaultClient(logger, testingCfg) mutex.Lock() defer mutex.Unlock() vaultTestCfg.tokenRevoked = true tokenRenewalErrorsTotal.Reset() client.renewalLoop() metricTokenRenewalErrorsTotal, _ := tokenRenewalErrorsTotal.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName, vaultLookupSelfOperationName, errors.UnknownErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricTokenRenewalErrorsTotal)) } func TestRenewalLoopNotRenewableToken(t *testing.T) { client, _ := vaultClient(logger, testingCfg) mutex.Lock() defer mutex.Unlock() vaultTestCfg.tokenRenewable = false vaultTestCfg.tokenRevoked = false vaultTestCfg.tokenTTL = 600 client.maxTokenTTL = 6000 tokenRenewalErrorsTotal.Reset() client.renewalLoop() metricTokenRenewalErrorsTotal, _ := tokenRenewalErrorsTotal.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName, vaultIsRenewableOperationName, errors.VaultTokenNotRenewableErrorType) assert.Equal(t, 1.0, testutil.ToFloat64(metricTokenRenewalErrorsTotal)) } func TestRenewalLoopInvalidRoleId(t *testing.T) { client, _ := vaultClient(logger, testingCfg) mutex.Lock() defer mutex.Unlock() vaultTestCfg.invalidRoleID = true vaultTestCfg.tokenRevoked = true tokenRenewalErrorsTotal.Reset() loginErrorsTotal.Reset() client.renewalLoop() loginErrorsTotal, _ := loginErrorsTotal.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName) assert.Equal(t, 1.0, testutil.ToFloat64(loginErrorsTotal)) vaultTestCfg.invalidRoleID = defaultInvalidAppRole vaultTestCfg.tokenRevoked = defaultRevokedToken } func TestRenewalLoopInvalidSecretId(t *testing.T) { client, _ := vaultClient(logger, testingCfg) mutex.Lock() defer mutex.Unlock() vaultTestCfg.invalidSecretID = true vaultTestCfg.tokenRevoked = true tokenRenewalErrorsTotal.Reset() loginErrorsTotal.Reset() client.renewalLoop() loginErrorsTotal, _ := loginErrorsTotal.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName) assert.Equal(t, 1.0, testutil.ToFloat64(loginErrorsTotal)) vaultTestCfg.invalidSecretID = defaultInvalidAppRole vaultTestCfg.tokenRevoked = defaultRevokedToken } func TestReadSecretKv2(t *testing.T) { client, _ := vaultClient(logger, testingCfg) secretValue, err := client.ReadSecret("/secret/data/test", "foo") assert.Nil(t, err) assert.Equal(t, "bar", secretValue) } func TestReadSecretKv1(t *testing.T) { mutex.Lock() defer mutex.Unlock() testingCfg.VaultEngine = "kv1" client, _ := vaultClient(logger, testingCfg) secretValue, err := client.ReadSecret("/secret/test", "foo") assert.Nil(t, err) assert.Equal(t, "bar", secretValue) } func TestSecretNotFound(t *testing.T) { client, _ := vaultClient(logger, testingCfg) path := "/secret/data/test" key := "foo2" secretReadErrorsTotal.Reset() secretValue, err := client.ReadSecret(path, key) metricSecretReadErrorsTotal, _ := secretReadErrorsTotal.GetMetricWithLabelValues(testingCfg.VaultURL, testingCfg.VaultEngine, vaultFakeVersion, vaultFakeClusterID, vaultFakeClusterName, path, key, errors.BackendSecretNotFoundErrorType) assert.Empty(t, secretValue) assert.EqualError(t, err, fmt.Sprintf("[%s] secret key %s not found at %s", errors.BackendSecretNotFoundErrorType, key, path)) assert.Equal(t, 1.0, testutil.ToFloat64(metricSecretReadErrorsTotal)) } ================================================ FILE: config/crd/bases/secrets-manager.tuenti.io_secretdefinitions.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.4.1 creationTimestamp: null name: secretdefinitions.secrets-manager.tuenti.io spec: group: secrets-manager.tuenti.io names: kind: SecretDefinition listKind: SecretDefinitionList plural: secretdefinitions singular: secretdefinition scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: SecretDefinition is the Schema for the secretdefinitions 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: SecretDefinitionSpec defines the desired state of SecretDefinition properties: keysMap: additionalProperties: description: DataSource represents the actual source of truth path for a secret properties: encoding: description: Encoding type for the secret. Only base64 supported. Optional type: string key: description: Key where the actual secret is stored type: string path: description: Path to the actual secret type: string required: - key - path type: object type: object name: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "make" to regenerate code after modifying this file' type: string type: type: string required: - keysMap - name type: object status: description: SecretDefinitionStatus defines the observed state of SecretDefinition type: object type: object served: true storage: true status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: [] ================================================ FILE: config/crd/kustomization.yaml ================================================ # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: - bases/secrets-manager.tuenti.io_secretdefinitions.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_secretdefinitions.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- patches/cainjection_in_secretdefinitions.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. configurations: - kustomizeconfig.yaml ================================================ FILE: config/crd/kustomizeconfig.yaml ================================================ # This file is for teaching kustomize how to substitute name and namespace reference in CRD nameReference: - kind: Service version: v1 fieldSpecs: - kind: CustomResourceDefinition version: v1 group: apiextensions.k8s.io path: spec/conversion/webhook/clientConfig/service/name namespace: - kind: CustomResourceDefinition version: v1 group: apiextensions.k8s.io path: spec/conversion/webhook/clientConfig/service/namespace create: false varReference: - path: metadata/annotations ================================================ FILE: config/crd/patches/cainjection_in_secretdefinitions.yaml ================================================ # The following patch adds a directive for certmanager to inject CA into the CRD apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) name: secretdefinitions.secretsmanager.secrets-manager.tuenti.io ================================================ FILE: config/crd/patches/webhook_in_secretdefinitions.yaml ================================================ # The following patch enables a conversion webhook for the CRD apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: secretsmanager.secrets-manager.tuenti.io spec: conversion: strategy: Webhook webhook: clientConfig: service: namespace: system name: webhook-service path: /convert conversionReviewVersions: - v1 ================================================ FILE: config/default/kustomization.yaml ================================================ # Adds namespace to all resources. namespace: secrets-manager-system # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. namePrefix: secrets-manager- # Labels to add to all resources and selectors. #commonLabels: # someName: someValue bases: - ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus patchesStrategicMerge: - manager_image_patch.yaml # Protect the /metrics endpoint by putting it behind auth. # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. #- manager_auth_proxy_patch.yaml # Mount the controller config file for loading manager configurations # through a ComponentConfig type #- manager_config_patch.yaml # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection #- webhookcainjection_patch.yaml # the following config is for teaching kustomize how to do var substitution vars: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR # objref: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # this name should match the one in certificate.yaml # fieldref: # fieldpath: metadata.namespace #- name: CERTIFICATE_NAME # objref: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # this name should match the one in certificate.yaml #- name: SERVICE_NAMESPACE # namespace of the service # objref: # kind: Service # version: v1 # name: webhook-service # fieldref: # fieldpath: metadata.namespace #- name: SERVICE_NAME # objref: # kind: Service # version: v1 # name: webhook-service ================================================ FILE: config/default/manager_auth_proxy_patch.yaml ================================================ # This patch inject a sidecar container which is a HTTP proxy for the # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system spec: template: spec: containers: - name: kube-rbac-proxy image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 args: - "--secure-listen-address=0.0.0.0:8443" - "--upstream=http://127.0.0.1:8080/" - "--logtostderr=true" - "--v=10" ports: - containerPort: 8443 name: https - name: manager args: - "--health-probe-bind-address=:8081" - "--metrics-bind-address=127.0.0.1:8080" - "--leader-elect" ================================================ FILE: config/default/manager_config_patch.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system spec: template: spec: containers: - name: manager args: - "--config=controller_manager_config.yaml" volumeMounts: - name: manager-config mountPath: /controller_manager_config.yaml subPath: controller_manager_config.yaml volumes: - name: manager-config configMap: name: manager-config ================================================ FILE: config/default/manager_image_patch.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system spec: template: spec: containers: # Change the value of image field below to your controller image URL - image: registry.hub.docker.com/tuentitech/secrets-manager:v2.1.0 name: manager ================================================ FILE: config/manager/controller_manager_config.yaml ================================================ apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 kind: ControllerManagerConfig health: healthProbeBindAddress: :8081 metrics: bindAddress: 127.0.0.1:8080 webhook: port: 9443 leaderElection: leaderElect: true resourceName: 5ac9a181.secrets-manager.tuenti.io ================================================ FILE: config/manager/kustomization.yaml ================================================ resources: - manager.yaml generatorOptions: disableNameSuffixHash: true configMapGenerator: - name: manager-config files: - controller_manager_config.yaml ================================================ FILE: config/manager/manager.yaml ================================================ apiVersion: v1 kind: Namespace metadata: labels: control-plane: controller-manager name: system --- apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system labels: control-plane: controller-manager spec: selector: matchLabels: control-plane: controller-manager replicas: 1 template: metadata: labels: control-plane: controller-manager spec: securityContext: runAsNonRoot: true containers: - command: - /manager args: - --leader-elect image: controller:latest name: manager securityContext: allowPrivilegeEscalation: false livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 resources: limits: cpu: 100m memory: 30Mi requests: cpu: 100m memory: 20Mi serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 ================================================ FILE: config/prometheus/kustomization.yaml ================================================ resources: - monitor.yaml ================================================ FILE: config/prometheus/monitor.yaml ================================================ # Prometheus Monitor Service (Metrics) apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: control-plane: controller-manager name: controller-manager-metrics-monitor namespace: system spec: endpoints: - path: /metrics port: https scheme: https bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager ================================================ FILE: config/rbac/auth_proxy_client_clusterrole.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-reader rules: - nonResourceURLs: - "/metrics" verbs: - get ================================================ FILE: config/rbac/auth_proxy_role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: proxy-role rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create ================================================ FILE: config/rbac/auth_proxy_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: proxy-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: proxy-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: config/rbac/auth_proxy_service.yaml ================================================ apiVersion: v1 kind: Service metadata: labels: control-plane: controller-manager name: controller-manager-metrics-service namespace: system spec: ports: - name: https port: 8443 targetPort: https selector: control-plane: controller-manager ================================================ FILE: config/rbac/kustomization.yaml ================================================ resources: # All RBAC will be applied under this service account in # the deployment namespace. You may comment out this resource # if your manager will use a service account that exists at # runtime. Be sure to update RoleBinding and ClusterRoleBinding # subjects if changing service account names. - service_account.yaml - role.yaml - role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # Comment the following 4 lines if you want to disable # the auth proxy (https://github.com/brancz/kube-rbac-proxy) # which protects your /metrics endpoint. - auth_proxy_service.yaml - auth_proxy_role.yaml - auth_proxy_role_binding.yaml - auth_proxy_client_clusterrole.yaml ================================================ FILE: config/rbac/leader_election_role.yaml ================================================ # permissions to do leader election. apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: leader-election-role rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ================================================ FILE: config/rbac/leader_election_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: leader-election-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: leader-election-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: config/rbac/role.yaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: creationTimestamp: null name: manager-role rules: - apiGroups: - "" resources: - secrets verbs: - create - delete - get - list - patch - update - watch - apiGroups: - secrets-manager.tuenti.io resources: - secretdefinitions verbs: - create - delete - get - list - patch - update - watch - apiGroups: - secrets-manager.tuenti.io resources: - secretdefinitions/finalizers verbs: - update - apiGroups: - secrets-manager.tuenti.io resources: - secretdefinitions/status verbs: - get - patch - update ================================================ FILE: config/rbac/role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: manager-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: config/rbac/secretdefinition_editor_role.yaml ================================================ # permissions for end users to edit secretdefinitions. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: secretdefinition-editor-role rules: - apiGroups: - secretsmanager.secrets-manager.tuenti.io resources: - secretdefinitions verbs: - create - delete - get - list - patch - update - watch - apiGroups: - secretsmanager.secrets-manager.tuenti.io resources: - secretdefinitions/status verbs: - get ================================================ FILE: config/rbac/secretdefinition_viewer_role.yaml ================================================ # permissions for end users to view secretdefinitions. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: secretdefinition-viewer-role rules: - apiGroups: - secretsmanager.secrets-manager.tuenti.io resources: - secretdefinitions verbs: - get - list - watch - apiGroups: - secretsmanager.secrets-manager.tuenti.io resources: - secretdefinitions/status verbs: - get ================================================ FILE: config/rbac/service_account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: controller-manager namespace: system ================================================ FILE: config/samples/README.md ================================================ ### Deployment sample This examples allows you to deploy vault and secrets-manager in your own cluster, using microk8s. 1.- Deploy vault `kubectl apply -f vault.yaml` 2.- Expose Vault port locally `kubectl port-forward $(kubectl get po -l app=vault| awk '{print $1}' | grep -v NAME) 8200:8200` 3.- Get Vault token `kubectl logs -l app=vault --tail=500 | grep Root` 4.- Vault setup This will create the policy, the role and a kubernetes secret containing role_id and secret_id. `VAULT_TOKEN= ./vault-setup.sh` 5.- Install crd `kubectl apply -f crd.yaml` 6.- Deploy secrets-manager `kubectl apply -f secrets-manager.yaml` *NOTE*: You have a `SecretDefinition` example there too to play with it: `secretsmanager_v1alpha1_secretdefinition.yaml` ================================================ FILE: config/samples/crd.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.4.1 creationTimestamp: null name: secretdefinitions.secrets-manager.tuenti.io spec: group: secrets-manager.tuenti.io names: kind: SecretDefinition listKind: SecretDefinitionList plural: secretdefinitions singular: secretdefinition scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: SecretDefinition is the Schema for the secretdefinitions 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: SecretDefinitionSpec defines the desired state of SecretDefinition properties: keysMap: additionalProperties: description: DataSource represents the actual source of truth path for a secret properties: encoding: description: Encoding type for the secret. Only base64 supported. Optional type: string key: description: Key where the actual secret is stored type: string path: description: Path to the actual secret type: string required: - key - path type: object type: object name: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "make" to regenerate code after modifying this file' type: string type: type: string required: - keysMap - name type: object status: description: SecretDefinitionStatus defines the observed state of SecretDefinition type: object type: object served: true storage: true status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: [] ================================================ FILE: config/samples/secrets-manager.yaml ================================================ --- apiVersion: v1 kind: ServiceAccount metadata: labels: app: secrets-manager name: secrets-manager namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: secrets-manager labels: app: secrets-manager rules: - apiGroups: - "" - "secrets-manager.tuenti.io" resources: - "secrets" - "secretdefinitions" verbs: - "get" - "list" - "watch" - "update" - "delete" - "create" --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: secrets-manager namespace: default labels: app: secrets-manager rules: - apiGroups: - "" resources: - "configmaps" verbs: - "get" - "list" - "watch" - "create" - "update" - apiGroups: - "coordination.k8s.io" resources: - leases verbs: - get - create - update - delete --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: secrets-manager labels: app: secrets-manager roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: secrets-manager subjects: - kind: ServiceAccount name: secrets-manager namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: secrets-manager namespace: default labels: app: secrets-manager roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: secrets-manager subjects: - kind: ServiceAccount name: secrets-manager namespace: default --- apiVersion: v1 kind: ConfigMap metadata: name: secrets-manager-config namespace: default data: secretDefinitions: |- - name: supersecret1 type: kubernetes.io/tls namespaces: - default data: tls.crt: encoding: base64 path: secret/data/pathtosecret1 key: value tls.key: encoding: base64 path: secret/data/pathtosecret3 key: value - name: supersecret2 type: Opaque namespaces: - default data: value1: path: secret/data/pathtosecret1 key: value value2: path: secret/data/pathtosecret2 key: value --- apiVersion: apps/v1 kind: Deployment metadata: annotations: deployment.kubernetes.io/revision: "1" labels: app: secrets-manager name: secrets-manager namespace: default spec: replicas: 1 selector: matchLabels: app: secrets-manager template: metadata: creationTimestamp: null labels: app: secrets-manager spec: serviceAccountName: secrets-manager containers: - image: secrets-manager:v1.1.0 imagePullPolicy: IfNotPresent name: secrets-manager args: - -vault.url=http://vault:8200 - -zap-log-level=debug env: - name: VAULT_ROLE_ID valueFrom: secretKeyRef: name: vault-approle-secret key: role_id - name: VAULT_SECRET_ID valueFrom: secretKeyRef: name: vault-approle-secret key: secret_id dnsPolicy: ClusterFirst restartPolicy: Always ================================================ FILE: config/samples/secretsmanager_v1alpha1_secretdefinition.yaml ================================================ --- apiVersion: secrets-manager.tuenti.io/v1alpha1 kind: SecretDefinition metadata: name: secretdefinition-sample spec: # Add fields here name: supersecretnew keysMap: decoded: path: secret/data/pathtosecret1 encoding: base64 key: value raw: path: secret/data/pathtosecret1 key: value ================================================ FILE: config/samples/vault-setup.sh ================================================ #!/bin/sh export VAULT_ADDR=http://localhost:8200 echo "Waiting vault to launch on 8200..." while ! nc -z localhost 8200; do sleep 0.1 # wait for 1/10 of the second before check again done echo "Vault launched" echo "Enabling approle" vault auth enable approle echo "Creating vault policy" cat > secrets-manager.hcl </dev/null || true kubectl create secret generic vault-approle-secret --from-literal role_id=$(vault read --field role_id auth/approle/role/secrets-manager/role-id) --from-literal secret_id=$(vault write --field secret_id -force auth/approle/role/secrets-manager/secret-id) ================================================ FILE: config/samples/vault.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: vault labels: app: vault spec: ports: - name: vault port: 8200 targetPort: 8200 protocol: TCP selector: app: vault --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: vault name: vault spec: selector: matchLabels: app: vault replicas: 1 template: metadata: labels: app: vault spec: containers: - image: vault name: vault command: - vault args: - server - -dev - -dev-listen-address=0.0.0.0:8200 ports: - containerPort: 8200 name: vaultport protocol: TCP volumeMounts: - name: root-home mountPath: /root env: - name: VAULT_ADDR value: http://localhost:8200 volumes: - name: root-home emptyDir: {} ================================================ FILE: controllers/metrics.go ================================================ package controllers import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "sigs.k8s.io/controller-runtime/pkg/metrics" ) var ( registry prometheus.Registry // Prometeheus metrics: https://prometheus.io secretReadErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: "secrets_manager", Subsystem: "controller", Name: "secret_read_errors_total", Help: "Errors total count when reading a secret from Kubernetes", }, []string{"namespace", "name"}) secretSyncErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: "secrets_manager", Subsystem: "controller", Name: "sync_errors_total", Help: "Secrets synchronization total errors.", }, []string{"namespace", "name"}) secretLastSyncStatus = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "secrets_manager", Subsystem: "controller", Name: "last_sync_status", Help: "The result of the last sync of a secret. 1 = OK, 0 = Error", }, []string{"namespace", "name"}) ) func init() { r := metrics.Registry r.MustRegister(secretReadErrorsTotal) r.MustRegister(secretSyncErrorsTotal) r.MustRegister(secretLastSyncStatus) } ================================================ FILE: controllers/secretdefinition_controller.go ================================================ /* Copyright 2021. 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 controllers import ( "context" "fmt" "reflect" "time" "github.com/go-logr/logr" smv1alpha1 "github.com/tuenti/secrets-manager/api/v1alpha1" "github.com/tuenti/secrets-manager/backend" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) const ( // https://golang.org/pkg/time/#pkg-constants timestampFormat = "2006-01-02T15.04.05Z" finalizerName = "secret.finalizer." + smv1alpha1.Group managedByLabel = "app.kubernetes.io/managed-by" lastUpdateLabel = smv1alpha1.Group + "/lastUpdateTime" ) // SecretDefinitionReconciler reconciles a SecretDefinition object type SecretDefinitionReconciler struct { client.Client Backend backend.Client Log logr.Logger APIReader client.Reader ReconciliationPeriod time.Duration ExcludeNamespaces map[string]bool Scheme *runtime.Scheme } // Annotations to skip when copying from a SecretDef to a Secret var annotationsToSkip = make(map[string]bool) // Helper functions to merge labels and annotations type skipfn func(string) bool func noSkip(_ string) bool { return false } func skipAnnotation(key string) bool { return annotationsToSkip[key] } func mergeMap(dst map[string]string, srcMap map[string]string, skipKey skipfn) { for k, v := range srcMap { if skipKey(k) { continue } dst[k] = v } } func getSecretFromSecretDefinition(sDef *smv1alpha1.SecretDefinition, data map[string][]byte) *corev1.Secret { objectMeta := getObjectMetaFromSecretDefinition(sDef) return &corev1.Secret{ Type: corev1.SecretType(sDef.Spec.Type), ObjectMeta: objectMeta, Data: data, } } // Helper functions to check and remove string from a slice of strings. func containsString(slice []string, s string) bool { for _, item := range slice { if item == s { return true } } return false } func removeString(slice []string, s string) (result []string) { for _, item := range slice { if item == s { continue } result = append(result, item) } return } // Ignore not found errors func ignoreNotFoundError(err error) error { if errors.IsNotFound(err) { return nil } return err } // isNotMarkedForRemoval will determine if the SecretDefinition object has been marked to be deleted func isNotMarkedForRemoval(sDef smv1alpha1.SecretDefinition) bool { return sDef.ObjectMeta.DeletionTimestamp.IsZero() } // getDesiredState reads the content from the Datasource for later comparison func (r *SecretDefinitionReconciler) getDesiredState(keysMap map[string]smv1alpha1.DataSource) (map[string][]byte, error) { desiredState := make(map[string][]byte) var err error for k, v := range keysMap { bSecret, err := r.Backend.ReadSecret(v.Path, v.Key) if err != nil { r.Log.Error(err, "unable to read secret from backend", "path", v.Path, "key", v.Key) return nil, err } decoder, err := backend.NewDecoder(v.Encoding) if err != nil { r.Log.Error(err, "refusing to use encoding", "encoding", v.Encoding) return nil, err } desiredState[k], err = decoder.DecodeString(bSecret) if err != nil { r.Log.Error(err, "unable to decode data for secret", "encoding", v.Encoding, "path", v.Path, "key", v.Key) return nil, err } } return desiredState, err } // getCurrentState reads the content from the Kubernetes Secret API object for later comparison func (r *SecretDefinitionReconciler) getCurrentState(ctx context.Context, namespace string, name string) (map[string][]byte, error) { // We don't read secrets from cache, as it's not the object we reconcile reader := r.APIReader data := make(map[string][]byte) secret := &corev1.Secret{} err := reader.Get(ctx, client.ObjectKey{ Namespace: namespace, Name: name, }, secret) if err != nil { secretReadErrorsTotal.WithLabelValues(name, namespace).Inc() return data, err } data = secret.Data return data, err } // upsertSecret will create or update a secret func (r *SecretDefinitionReconciler) upsertSecret(ctx context.Context, sDef *smv1alpha1.SecretDefinition, data map[string][]byte) error { secret := getSecretFromSecretDefinition(sDef, data) err := r.Create(ctx, secret) if errors.IsAlreadyExists(err) { err = r.Update(ctx, secret) } return err } // deleteSecret will delete a secret given its namespace and name func (r *SecretDefinitionReconciler) deleteSecret(ctx context.Context, namespace string, name string) error { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, }, } return r.Delete(ctx, secret) } // shouldExclude will return true if the secretDefinition is in an excluded namespace func (r *SecretDefinitionReconciler) shouldExclude(sDefNamespace string) bool { if len(r.ExcludeNamespaces) > 0 { return r.ExcludeNamespaces[sDefNamespace] } return false } // AddFinalizerIfNotPresent will check if finalizerName is the finalizers slice func (r *SecretDefinitionReconciler) AddFinalizerIfNotPresent(ctx context.Context, sDef *smv1alpha1.SecretDefinition, finalizerName string) error { if !containsString(sDef.ObjectMeta.Finalizers, finalizerName) { sDef.ObjectMeta.Finalizers = append(sDef.ObjectMeta.Finalizers, finalizerName) return r.Update(ctx, sDef) } return nil } // Helper functions to manage corev1.Secret and smv1alpha1.SecretDefinition func getObjectMetaFromSecretDefinition(sDef *smv1alpha1.SecretDefinition) metav1.ObjectMeta { labels := map[string]string{ managedByLabel: "secrets-manager", } annotations := map[string]string{ lastUpdateLabel: time.Now().Format(timestampFormat), } mergeMap(labels, sDef.Labels, noSkip) mergeMap(annotations, sDef.Annotations, skipAnnotation) return metav1.ObjectMeta{ Namespace: sDef.Namespace, Name: sDef.Spec.Name, Labels: labels, Annotations: annotations, } } //+kubebuilder:rbac:groups=secrets-manager.tuenti.io,resources=secretdefinitions,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=secrets-manager.tuenti.io,resources=secretdefinitions/status,verbs=get;update;patch //+kubebuilder:rbac:groups=secrets-manager.tuenti.io,resources=secretdefinitions/finalizers,verbs=update //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // TODO(user): Modify the Reconcile function to compare the state specified by // the SecretDefinition object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile func (r *SecretDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("secretdefinition", req.NamespacedName) sDef := &smv1alpha1.SecretDefinition{} err := r.Get(ctx, req.NamespacedName, sDef) if err != nil { log.Error(err, fmt.Sprintf("could not get SecretDefinition '%s'", req.NamespacedName)) return ctrl.Result{}, ignoreNotFoundError(err) } secretName := sDef.Spec.Name secretNamespace := sDef.Namespace log = log.WithValues("secret", fmt.Sprintf("%s/%s", secretNamespace, secretName)) if isNotMarkedForRemoval(*sDef) { err = r.AddFinalizerIfNotPresent(ctx, sDef, finalizerName) if err != nil { log.Error(err, "unable to update SecretDefinition finalizers", "finalizer", finalizerName) return ctrl.Result{}, err } if r.shouldExclude(sDef.Namespace) { log.Info("Secret definition in excluded namespace, ignoring", "excluded_namespaces", r.ExcludeNamespaces) return ctrl.Result{}, nil } // Get data from the secret source of truth desiredState, err := r.getDesiredState(sDef.Spec.KeysMap) if err != nil { log.Error(err, "unable to get desired state for secret") secretSyncErrorsTotal.WithLabelValues(secretNamespace, secretName).Inc() secretLastSyncStatus.WithLabelValues(secretNamespace, secretName).Set(0.0) return ctrl.Result{}, err } // Get the actual secret from Kubernetes currentState, err := r.getCurrentState(ctx, secretNamespace, secretName) if err != nil && !errors.IsNotFound(err) { log.Error(err, "unable to get current state of secret") secretSyncErrorsTotal.WithLabelValues(secretNamespace, secretName).Inc() secretLastSyncStatus.WithLabelValues(secretNamespace, secretName).Set(0.0) return ctrl.Result{}, ignoreNotFoundError(err) } eq := reflect.DeepEqual(desiredState, currentState) if !eq { log.Info("secret must be updated") if err := r.upsertSecret(ctx, sDef, desiredState); err != nil { log.Error(err, "unable to upsert secret") secretSyncErrorsTotal.WithLabelValues(secretNamespace, secretName).Inc() secretLastSyncStatus.WithLabelValues(secretNamespace, secretName).Set(0.0) return ctrl.Result{}, err } log.Info("secret updated") } secretLastSyncStatus.WithLabelValues(secretNamespace, secretName).Set(1.0) return ctrl.Result{RequeueAfter: r.ReconciliationPeriod}, nil } else { // SecretDefinition has been marked for deletion and contains finalizer if containsString(sDef.ObjectMeta.Finalizers, finalizerName) { if err = r.deleteSecret(ctx, secretNamespace, secretName); err != nil && !errors.IsNotFound(err) { log.Error(err, "unable to delete secret") return ctrl.Result{}, ignoreNotFoundError(err) } log.Info("secret deleted successfully") // If success remove finalizer sDef.ObjectMeta.Finalizers = removeString(sDef.ObjectMeta.Finalizers, finalizerName) if err = r.Update(ctx, sDef); err != nil { log.Error(err, "unable to remove finalizer from SecretDefinition", "finalizer", finalizerName) return ctrl.Result{}, err } } return ctrl.Result{}, nil } } // SetupWithManager sets up the controller with the Manager. func (r *SecretDefinitionReconciler) SetupWithManager(mgr ctrl.Manager, name string) error { return ctrl.NewControllerManagedBy(mgr). For(&smv1alpha1.SecretDefinition{}). Named(name). Complete(r) } func init() { // last-applied-configuration should not be copied from the SecretDef to the Secret annotationsToSkip[corev1.LastAppliedConfigAnnotation] = true } ================================================ FILE: controllers/secretdefinition_controller_test.go ================================================ package controllers import ( "context" "encoding/base64" "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" smv1alpha1 "github.com/tuenti/secrets-manager/api/v1alpha1" "github.com/tuenti/secrets-manager/errors" "reflect" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const ( encodedValue = "bG9yZW0gaXBzdW0gZG9ybWEK" decodedValue = "lorem ipsum dorma" ) var _ = Describe("SecretsManager", func() { var ( //cfg *rest.Config r *SecretDefinitionReconciler decodedBytes, _ = base64.StdEncoding.DecodeString(encodedValue) anyData = map[string][]byte{"foo": decodedBytes} sd = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "secret-test", Labels: map[string]string{ "test.example.com/name": "test", "name": "secret_labels", }, Annotations: map[string]string{ "ann1": "another_value", "ann2": "just_a_value", }, }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-test", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "foo": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base64", }, }, }, } sdWithSkipAnnotations = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "secret-test", Labels: map[string]string{ "test.example.com/name": "test", "name": "secret_labels", }, Annotations: map[string]string{ "ann1": "another_value", "ann2": "just_a_value", corev1.LastAppliedConfigAnnotation: "to_be_skipped", }, }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-test", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "foo": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base64", }, }, }, } sd2 = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "secret-test2", }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-test2", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "foo2": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base64", }, }, }, } sdNotWatched = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "notwatched", Name: "secret-notwatched", }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-notwatched", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "notwatched": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base64", }, }, }, } sdWatched = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "watched", Name: "secret-watched", }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-watched", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "watched": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base64", }, }, }, } sdMultiWatched1 = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "watched1", Name: "secret-multi1", }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-multi1", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "multival1": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base64", }, }, }, } sdMultiWatched2 = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "watched2", Name: "secret-multi2", }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-multi2", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "multival2": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base64", }, }, }, } sdBackendSecretNotFound = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "secret-beckend-secret-not-found", }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-backend-secret-not-found", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "foo3": { Path: "secret/data/notfound", Key: "value", Encoding: "base64", }, }, }, } sdWrongEncoding = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "secret-wrong-encoding", }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-wrong-encoding", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "foo4": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base65", }, }, }, } sdExcludedNs = &smv1alpha1.SecretDefinition{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "secret-excluded-ns", }, Spec: smv1alpha1.SecretDefinitionSpec{ Name: "secret-excluded-ns", Type: "Opaque", KeysMap: map[string]smv1alpha1.DataSource{ "fooExcludedNs": { Path: "secret/data/pathtosecret1", Key: "value", Encoding: "base64", }, }, }, } ) BeforeEach(func() { r = getReconciler() cfg = getConfig() }) AfterEach(func() { }) Context("SecretDefinitionReconciler.Reconcile", func() { It("Create a secretdefinition and read the secret", func() { // setup: secretdefinition := sd ctx := context.Background() // when: err := r.Create(ctx, secretdefinition) res, err2 := r.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: secretdefinition.Namespace, Name: secretdefinition.Name, }, }) data, err3 := r.getCurrentState(ctx, "default", secretdefinition.ObjectMeta.Name) print(data) // then: Expect(err).To(BeNil()) Expect(res).ToNot(BeNil()) Expect(err2).To(BeNil()) Expect(err3).To(BeNil()) Expect(data).To(Equal(anyData)) //Expect(data).To(HaveKey("finalizers")) //("finalizers", "secret.finalizer.secrets-manager.tuenti.io")) }) It("Delete a secretdefinition should delete a secret", func() { // setup: secretdefinition := sd2 ctx := context.Background() // when: err := r.Create(ctx, secretdefinition) res, err2 := r.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: secretdefinition.Namespace, Name: secretdefinition.Name, }, }) // then: Expect(err).To(BeNil()) Expect(res).ToNot(BeNil()) Expect(err2).To(BeNil()) //Expect(secretdefinition.ObjectMeta.Finalizers).To(BeEmpty()) // when: data, err3 := r.getCurrentState(ctx, "default", secretdefinition.ObjectMeta.Name) // then: Expect(err3).To(BeNil()) Expect(data).To(Equal(map[string][]byte{"foo2": decodedBytes})) // when: err4 := r.Delete(ctx, secretdefinition) _, err5 := r.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: secretdefinition.Namespace, Name: secretdefinition.Name, }, }) data2, err6 := r.getCurrentState(ctx, "default", secretdefinition.ObjectMeta.Name) // then: Expect(err4).To(BeNil()) Expect(err5).To(BeNil()) Expect(err6).ToNot(BeNil()) Expect(data2).To(BeEmpty()) }) It("Create a secretdefinition with a secret not deployed in the backend", func() { ctx := context.Background() err := r.Create(ctx, sdBackendSecretNotFound) Expect(err).To(BeNil()) res, err2 := r.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: sdBackendSecretNotFound.Namespace, Name: sdBackendSecretNotFound.Name, }, }) Expect(err2).ToNot(BeNil()) Expect(res).To(Equal(reconcile.Result{})) }) It("Create a secretdefinition with a wrong encoding", func() { ctx := context.Background() expectedErr := &errors.EncodingNotImplementedError{} err := r.Create(ctx, sdWrongEncoding) Expect(err).To(BeNil()) res, err2 := r.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: sdWrongEncoding.Namespace, Name: sdWrongEncoding.Name, }, }) Expect(reflect.TypeOf(err2)).To(Equal(reflect.TypeOf(expectedErr))) Expect(res).To(Equal(reconcile.Result{})) }) It("Create a secretdefinition in a excluded namespace", func() { // setup: secretdefinition := sdExcludedNs r2 := getReconciler() r2.ExcludeNamespaces = map[string]bool{secretdefinition.Namespace: true} ctx := context.Background() // when: err := r.Create(ctx, secretdefinition) res, err2 := r.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{ Namespace: secretdefinition.Namespace, Name: secretdefinition.Name, }, }) // then: Expect(err).To(BeNil()) Expect(err2).To(BeNil()) Expect(res).To(Equal(reconcile.Result{})) }) }) Context("SecretDefinitionReconciler.upsertSecret", func() { It("Upsert a secret twice should not raise an error", func() { // setup: secretdefinition := sd ctx := context.Background() // when: err := r.upsertSecret(ctx, secretdefinition, anyData) err2 := r.upsertSecret(ctx, secretdefinition, anyData) // then: Expect(err).To(BeNil()) Expect(err2).To(BeNil()) }) It("Upsert a secret", func() { // setup: secretdefinition := sd ctx := context.Background() // when: err := r.upsertSecret(ctx, secretdefinition, anyData) // then: Expect(err).To(BeNil()) }) }) Context("SecretDefinitionReconciler.getObjectMetaFromSecretDefinition", func() { It("getObjectMetaFromSecretDefinition should return an ObjectMeta", func() { // when: objectMeta := getObjectMetaFromSecretDefinition(sd) // then: Expect(sd.Namespace).To(Equal(objectMeta.Namespace)) Expect(sd.Name).To(Equal(objectMeta.Name)) for k := range sd.Labels { Expect(objectMeta.Labels).Should(HaveKey(k)) } for k := range sd.Annotations { Expect(objectMeta.Annotations).Should(HaveKey(k)) } }) It("getObjectMetaFromSecretDefinition should add custom labels and annotations to the objectMeta", func() { // when: objectMeta := getObjectMetaFromSecretDefinition(sd) // then: Expect(objectMeta.Labels).Should(HaveKey("app.kubernetes.io/managed-by")) Expect(objectMeta.Annotations).Should(HaveKey("secrets-manager.tuenti.io/lastUpdateTime")) }) It("getObjectMetaFromSecretDefinition should skip expected annotations", func() { // when: objectMeta := getObjectMetaFromSecretDefinition(sdWithSkipAnnotations) // then: Expect(objectMeta.Labels).Should(Not(HaveKey(corev1.LastAppliedConfigAnnotation))) }) }) Context("Manager.MultiNamespacedCache", func() { It("Creates secret in watched namespace", func(done Done) { scheme := getScheme() decodedBytes, _ := base64.StdEncoding.DecodeString(encodedValue) mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, MetricsBindAddress: "0", LeaderElection: false, NewCache: cache.MultiNamespacedCacheBuilder([]string{"watched"}), }) Expect(err).ToNot(HaveOccurred()) Expect(mgr).ToNot(BeNil()) r2 := getReconciler() r2.SetupWithManager(mgr, "test1") // Stream generates values with DoSomething and sends them to out // until DoSomething returns an error or ctx.Done is closed. ctx, cancelfunc := context.WithCancel(context.Background()) go func() { defer GinkgoRecover() Expect(mgr.Start(ctx)).NotTo(HaveOccurred()) close(done) }() r2.Create(ctx, sdWatched) // Sleep for 4 * the reconcile interval set on the controller (just to be safe) time.Sleep(4 * time.Second) data, err := r2.getCurrentState(ctx, "watched", sdWatched.Spec.Name) Expect(err).To(BeNil()) Expect(data).To(Equal(map[string][]byte{"watched": decodedBytes})) cancelfunc() }, 10) It("Doesn't create secret in unwatched namespace", func(done Done) { scheme := getScheme() mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, MetricsBindAddress: "0", LeaderElection: false, NewCache: cache.MultiNamespacedCacheBuilder([]string{"watched"}), }) Expect(err).ToNot(HaveOccurred()) Expect(mgr).ToNot(BeNil()) r2 := getReconciler() r2.SetupWithManager(mgr, "test2") ctx, cancelfunc := context.WithCancel(context.Background()) go func() { defer GinkgoRecover() Expect(mgr.Start(ctx)).NotTo(HaveOccurred()) close(done) }() r2.Create(ctx, sdNotWatched) // Sleep for 4 * the reconcile interval set on the controller (just to be safe) time.Sleep(4 * time.Second) data, err := r2.getCurrentState(ctx, "notwatched", sdNotWatched.Spec.Name) Expect(err.Error()).To(Equal("secrets \"secret-notwatched\" not found")) Expect(data).To(BeEmpty()) cancelfunc() }, 10) It("Creates secrets in multiple watched namespaces", func(done Done) { scheme := getScheme() decodedBytes, _ := base64.StdEncoding.DecodeString(encodedValue) mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, MetricsBindAddress: "0", LeaderElection: false, NewCache: cache.MultiNamespacedCacheBuilder([]string{"watched1", "watched2"}), }) Expect(err).ToNot(HaveOccurred()) Expect(mgr).ToNot(BeNil()) r2 := getReconciler() r2.SetupWithManager(mgr, "test3") ctx, cancelfunc := context.WithCancel(context.Background()) go func() { defer GinkgoRecover() Expect(mgr.Start(ctx)).NotTo(HaveOccurred()) close(done) }() r2.Create(ctx, sdMultiWatched1) r2.Create(ctx, sdMultiWatched2) // Sleep for 4 * the reconcile interval set on the controller (just to be safe) time.Sleep(4 * time.Second) data, err2 := r2.getCurrentState(ctx, "watched1", sdMultiWatched1.Spec.Name) Expect(err2).To(BeNil()) Expect(data).To(Equal(map[string][]byte{"multival1": decodedBytes})) data2, err3 := r2.getCurrentState(ctx, "watched2", sdMultiWatched2.Spec.Name) Expect(err3).To(BeNil()) Expect(data2).To(Equal(map[string][]byte{"multival2": decodedBytes})) cancelfunc() }, 10) }) }) ================================================ FILE: controllers/suite_test.go ================================================ /* 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 controllers import ( "context" "errors" "path/filepath" "testing" "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" secretsmanagerv1alpha1 "github.com/tuenti/secrets-manager/api/v1alpha1" "k8s.io/client-go/rest" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" // +kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var cfg *rest.Config var k8sClient client.Client var r *SecretDefinitionReconciler var testEnv *envtest.Environment var mgr ctrl.Manager var scheme *runtime.Scheme type fakeBackendSecret struct { Path string Key string Content string } type fakeBackend struct { fakeSecrets []fakeBackendSecret } func newFakeBackend(fakeSecrets []fakeBackendSecret) fakeBackend { return fakeBackend{ fakeSecrets: fakeSecrets, } } func (f fakeBackend) ReadSecret(path string, key string) (string, error) { for _, fakeSecret := range f.fakeSecrets { if fakeSecret.Path == path && fakeSecret.Key == key { return fakeSecret.Content, nil } } return "", errors.New("Not found") } func getReconciler() *SecretDefinitionReconciler { return r } func getConfig() *rest.Config { return cfg } func getScheme() *runtime.Scheme { return scheme } func TestSecretDefinitionController(t *testing.T) { RegisterFailHandler(Fail) RunSpecsWithDefaultAndCustomReporters(t, "Controller Suite", []Reporter{printer.NewlineReporter{}}) } var _ = BeforeSuite(func(done Done) { namespaces := [...]string{"notwatched", "watched", "watched1", "watched2"} logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, } var err error cfg, err = testEnv.Start() Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) scheme = runtime.NewScheme() corev1.AddToScheme(scheme) secretsmanagerv1alpha1.AddToScheme(scheme) err = secretsmanagerv1alpha1.AddToScheme(scheme) Expect(err).ToNot(HaveOccurred()) // +kubebuilder:scaffold:scheme mgr, err = ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, MetricsBindAddress: "0", LeaderElection: false, }) Expect(err).ToNot(HaveOccurred()) Expect(mgr).ToNot(BeNil()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) Expect(err).ToNot(HaveOccurred()) for _, ns := range namespaces { nsSpec := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ""}} nsSpec.Name = ns k8sClient.Create(context.Background(), nsSpec) } r = &SecretDefinitionReconciler{ Backend: newFakeBackend([]fakeBackendSecret{ {"secret/data/pathtosecret1", "value", "bG9yZW0gaXBzdW0gZG9ybWEK"}, }), Client: k8sClient, APIReader: k8sClient, ReconciliationPeriod: 1 * time.Second, Log: logf.Log.WithName("controllers-test").WithName("SecretDefinition"), } err = r.SetupWithManager(mgr, "testing") //Expect(err).ToNot(HaveOccurred())*/ Expect(err).ToNot(HaveOccurred()) close(done) }, 60) var _ = AfterSuite(func() { By("tearing down the test environment") err := testEnv.Stop() Expect(err).ToNot(HaveOccurred()) }) ================================================ FILE: deploy/Dockerfile ================================================ # Build the manager binary FROM golang:1.16 as builder WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the go source COPY main.go main.go COPY api/ api/ COPY controllers/ controllers/ COPY backend/ backend/ COPY errors/ errors/ COPY hack/ hack/ ARG SECRETS_MANAGER_VERSION # Build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=${SECRETS_MANAGER_VERSION}" -a -o secrets-manager main.go # # Prod image # # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static:nonroot as release WORKDIR / COPY --from=builder /workspace/secrets-manager . USER 65532:65532 ENTRYPOINT ["/secrets-manager"] # # Dev image # FROM builder as dev ENV ENVTEST_ASSETS_DIR=testbin ENV ENVTEST_K8S_VERSION=1.19.2 # kubebuilder needed to run tests in development environment RUN curl -L -O https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.1.0/kubebuilder_linux_amd64 RUN mv kubebuilder_linux_amd64 kubebuilder \ && chmod 755 kubebuilder \ && mv kubebuilder /usr/local/bin RUN export PATH=$PATH:/usr/local/bin ================================================ FILE: deploy/version/get.sh ================================================ #!/bin/bash awk '{ split($0,parts,"="); print parts[2]}' $(pwd)/deploy/version/version.properties ================================================ FILE: deploy/version/update.sh ================================================ #!/bin/bash version_properties_file=$(pwd)/deploy/version/version.properties update_major() { new_version="v$(awk '{ split($0,parts,"=v"); split(parts[2],version,"."); print version[1]+1 "." version[2] "." version[3]}' $version_properties_file)"; echo "version=${new_version}" > $version_properties_file } update_minor() { new_version="v$(awk '{ split($0,parts,"=v"); split(parts[2],version,"."); print version[1] "." version[2]+1 "." version[3]}' $version_properties_file)"; echo "version=${new_version}" > $version_properties_file } update_patch() { new_version="v$(awk '{ split($0,parts,"=v"); split(parts[2],version,"."); print version[1] "." version[2] "." version[3]+1}' $version_properties_file)"; echo "version=${new_version}" > $version_properties_file } update_kind=$1 case $update_kind in "--major" | "-M") echo "Updating major version" update_major ;; "--minor" | "-m") echo "Updating minor version" update_minor ;; "--patch" | "-p") echo "Updating patch version" update_patch ;; *) update_minor ;; esac ================================================ FILE: deploy/version/version.properties ================================================ version=v2.1.0 ================================================ FILE: docker-compose.yaml ================================================ version: '3.4' services: secrets-manager-local: build: context: . target: dev dockerfile: ./deploy/Dockerfile volumes: - "./:/workspace" tests: build: context: . target: dev dockerfile: ./deploy/Dockerfile volumes: - "./:/workspace" command: > bash -c "pwd && mkdir -p controllers/$$ENVTEST_ASSETS_DIR && test -f controllers/$$ENVTEST_ASSETS_DIR/setup-envtest.sh || curl -sSLo controllers/$$ENVTEST_ASSETS_DIR/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.8.3/hack/setup-envtest.sh && source controllers/$$ENVTEST_ASSETS_DIR/setup-envtest.sh && fetch_envtest_tools controllers/$$ENVTEST_ASSETS_DIR && setup_envtest_env $$ENVTEST_ASSETS_DIR && go test -v ./backend... ./errors/... ./controllers/... -coverprofile cover.out" secrets-manager: build: context: . target: release dockerfile: ./deploy/Dockerfile ================================================ FILE: errors/errors.go ================================================ package errors import "fmt" // Error Types constants const ( UnknownErrorType = "UnknownError" BackendNotImplementedErrorType = "BackendNotImplementedError" BackendSecretNotFoundErrorType = "BackendSecretNotFoundError" BackendSecretForbiddenErrorType = "BackendSecretForbiddenError" K8sSecretNotFoundErrorType = "K8sSecretNotFoundError" EncodingNotImplementedErrorType = "EncodingNotImplementedError" VaultEngineNotImplementedErrorType = "VaultEngineNotImplementedError" VaultTokenNotRenewableErrorType = "VaultTokenNotRenewableError" ) // BackendNotImplementedError will be raised if the selected backend is not implemented type BackendNotImplementedError struct { ErrType string Backend string } // BackendSecretNotFoundError will be raised if secret is not found in the selected backend type BackendSecretNotFoundError struct { ErrType string Path string Key string } // K8sSecretNotFoundError will be raised if secret is not found by its name in the given namespace type K8sSecretNotFoundError struct { ErrType string Name string Namespace string } // EncodingNotImplementedError will be raised if the selected encoding is not implemented type EncodingNotImplementedError struct { ErrType string Encoding string } // VaultEngineNotImplementedError will be raised if the selected engine is not implemented type VaultEngineNotImplementedError struct { ErrType string Engine string } // VaultTokenNotRenewableError will be raised if secrets-manager Vault token is not renewable type VaultTokenNotRenewableError struct { ErrType string } func getErrorType(err error) string { switch err.(type) { case *BackendNotImplementedError: return BackendNotImplementedErrorType case *BackendSecretNotFoundError: return BackendSecretNotFoundErrorType case *K8sSecretNotFoundError: return K8sSecretNotFoundErrorType case *EncodingNotImplementedError: return EncodingNotImplementedErrorType case *VaultEngineNotImplementedError: return VaultEngineNotImplementedErrorType case *VaultTokenNotRenewableError: return VaultTokenNotRenewableErrorType default: return UnknownErrorType } } func (e BackendNotImplementedError) Error() string { return fmt.Sprintf("[%s] backend %s not supported", e.ErrType, e.Backend) } func (e BackendSecretNotFoundError) Error() string { return fmt.Sprintf("[%s] secret key %s not found at %s", e.ErrType, e.Key, e.Path) } func (e K8sSecretNotFoundError) Error() string { return fmt.Sprintf("[%s] secret '%s/%s' not found", e.ErrType, e.Namespace, e.Name) } func (e EncodingNotImplementedError) Error() string { return fmt.Sprintf("[%s] encoding %s not supported", e.ErrType, e.Encoding) } func (e VaultEngineNotImplementedError) Error() string { return fmt.Sprintf("[%s] vault engine %s not supported", e.ErrType, e.Engine) } func (e VaultTokenNotRenewableError) Error() string { return fmt.Sprintf("[%s] vault token not renewable", e.ErrType) } // IsBackendNotImplemented returns true if the error is type of BackendNotImplementedError and false otherwise func IsBackendNotImplemented(err error) bool { return getErrorType(err) == BackendNotImplementedErrorType } // IsBackendSecretNotFound returns true if the error is type of BackendSecretNotFound and false otherwise func IsBackendSecretNotFound(err error) bool { return getErrorType(err) == BackendSecretNotFoundErrorType } // IsK8sSecretNotFound returns true if the error is type of K8sSecretNotFound and false otherwise func IsK8sSecretNotFound(err error) bool { return getErrorType(err) == K8sSecretNotFoundErrorType } // IsEncodingNotImplemented returns true if the error is type of EncodingNotImplementedError and false otherwise func IsEncodingNotImplemented(err error) bool { return getErrorType(err) == EncodingNotImplementedErrorType } // IsVaultEngineNotImplemented returns true if the error is type of VaultEngineNotImplementedError and false otherwise func IsVaultEngineNotImplemented(err error) bool { return getErrorType(err) == VaultEngineNotImplementedErrorType } // IsVaultTokenNotRenewable returns true if the error is type of VaultTokenNotRenewableError and false otherwise func IsVaultTokenNotRenewable(err error) bool { return getErrorType(err) == VaultTokenNotRenewableErrorType } ================================================ FILE: errors/errors_test.go ================================================ package errors import ( e "errors" "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestErrorString(t *testing.T) { err1 := &BackendNotImplementedError{ErrType: BackendNotImplementedErrorType, Backend: "foo"} assert.EqualError(t, err1, fmt.Sprintf("[%s] backend %s not supported", err1.ErrType, err1.Backend)) err2 := &BackendSecretNotFoundError{ErrType: BackendSecretNotFoundErrorType, Path: "foo", Key: "bar"} assert.EqualError(t, err2, fmt.Sprintf("[%s] secret key %s not found at %s", err2.ErrType, err2.Key, err2.Path)) err3 := &K8sSecretNotFoundError{ErrType: K8sSecretNotFoundErrorType, Name: "foo", Namespace: "bar"} assert.EqualError(t, err3, fmt.Sprintf("[%s] secret '%s/%s' not found", err3.ErrType, err3.Namespace, err3.Name)) err5 := &EncodingNotImplementedError{ErrType: EncodingNotImplementedErrorType, Encoding: "foo"} assert.EqualError(t, err5, fmt.Sprintf("[%s] encoding %s not supported", err5.ErrType, err5.Encoding)) err6 := &VaultEngineNotImplementedError{ErrType: VaultEngineNotImplementedErrorType, Engine: "foo"} assert.EqualError(t, err6, fmt.Sprintf("[%s] vault engine %s not supported", err6.ErrType, err6.Engine)) err7 := &VaultTokenNotRenewableError{ErrType: VaultTokenNotRenewableErrorType} assert.EqualError(t, err7, fmt.Sprintf("[%s] vault token not renewable", err7.ErrType)) } func TestGetErrorType(t *testing.T) { err1 := e.New("foo") assert.Equal(t, getErrorType(err1), UnknownErrorType) err2 := &BackendNotImplementedError{ErrType: BackendNotImplementedErrorType} assert.Equal(t, getErrorType(err2), BackendNotImplementedErrorType) err3 := &BackendSecretNotFoundError{ErrType: BackendSecretNotFoundErrorType} assert.Equal(t, getErrorType(err3), BackendSecretNotFoundErrorType) err4 := &K8sSecretNotFoundError{ErrType: K8sSecretNotFoundErrorType} assert.Equal(t, getErrorType(err4), K8sSecretNotFoundErrorType) err6 := &EncodingNotImplementedError{ErrType: EncodingNotImplementedErrorType} assert.Equal(t, getErrorType(err6), EncodingNotImplementedErrorType) err7 := &VaultEngineNotImplementedError{ErrType: VaultEngineNotImplementedErrorType} assert.Equal(t, getErrorType(err7), VaultEngineNotImplementedErrorType) err8 := &VaultTokenNotRenewableError{ErrType: VaultTokenNotRenewableErrorType} assert.Equal(t, getErrorType(err8), VaultTokenNotRenewableErrorType) } func TestIsBackendNotImplemented(t *testing.T) { err := &BackendNotImplementedError{ErrType: BackendNotImplementedErrorType} assert.True(t, IsBackendNotImplemented(err)) err2 := e.New("foo") assert.False(t, IsBackendNotImplemented(err2)) } func TestIsBackendSecretNotFound(t *testing.T) { err := &BackendSecretNotFoundError{ErrType: BackendSecretNotFoundErrorType} assert.True(t, IsBackendSecretNotFound(err)) err2 := e.New("foo") assert.False(t, IsBackendSecretNotFound(err2)) } func TestIsK8sSecretNotFound(t *testing.T) { err := &K8sSecretNotFoundError{ErrType: K8sSecretNotFoundErrorType} assert.True(t, IsK8sSecretNotFound(err)) err2 := e.New("foo") assert.False(t, IsK8sSecretNotFound(err2)) } func TestIsEncodingNotImplemented(t *testing.T) { err := &EncodingNotImplementedError{ErrType: EncodingNotImplementedErrorType} assert.True(t, IsEncodingNotImplemented(err)) err2 := e.New("foo") assert.False(t, IsEncodingNotImplemented(err2)) } func TestIsVaultEngineNotImplemented(t *testing.T) { err := &VaultEngineNotImplementedError{ErrType: VaultEngineNotImplementedErrorType} assert.True(t, IsVaultEngineNotImplemented(err)) } func TestIsVaultTokenNotRenewable(t *testing.T) { err := &VaultTokenNotRenewableError{ErrType: VaultTokenNotRenewableErrorType} assert.True(t, IsVaultTokenNotRenewable(err)) } ================================================ FILE: go.mod ================================================ module github.com/tuenti/secrets-manager go 1.16 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2 github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.6.0 github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect github.com/go-logr/logr v0.3.0 github.com/gorilla/mux v1.7.4 github.com/hashicorp/vault/api v1.2.0 github.com/onsi/ginkgo v1.14.1 github.com/onsi/gomega v1.10.2 github.com/prometheus/client_golang v1.7.1 github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/net v0.0.0-20220403103023-749bd193bc2b k8s.io/api v0.20.2 k8s.io/apimachinery v0.20.2 k8s.io/client-go v0.20.2 sigs.k8s.io/controller-runtime v0.8.3 ) ================================================ FILE: go.sum ================================================ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= 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= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.20.0/go.mod h1:ZPW/Z0kLCTdDZaDbYTetxc9Cxl/2lNqxYHYNOF2bti0= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 h1:zBJcBJwte0x6PcPK7XaWDMvK2o2ZM2f1sMaqNNavQ5g= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.0/go.mod h1:TmXReXZ9yPp5D5TBRMTAtyz+UyOl15Py4hL5E5p6igQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2 h1:mM/yraAumqMMIYev6zX0oxHqX6hreUs5wXf76W47r38= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2/go.mod h1:+nVKciyKD2J9TyVcEQ82Bo9b+3F92PiQfHrIE/zqLqM= github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 h1:sLZ/Y+P/5RRtsXWylBjB5lkgixYfm0MQPiwrSX//JSo= github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.6.0 h1:bupm+qpGJpsHKCDBfrtUIQQuODnMLz9iey76vvw8q+c= github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.6.0/go.mod h1:mmFwM6gatrMBYRa2qirJkkehnh91KdrpAgcCT4IeD5s= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 h1:lirjIOHv5RrmDbZXw9lUz/fY68uU05qR4uIef58WMvQ= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1/go.mod h1:j1J9XXIo/eXD7YSrr73sYZTEY/AQ0+/Q6Aa96z1e2j8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.1 h1:eVvIXUKiTgv++6YnWb42DUA1YL7qDugnKP0HljexdnQ= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= 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.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= github.com/armon/go-metrics v0.3.3 h1:a9F4rlj7EWWrbj7BYw8J8+x+ZZkJeqzNyRk8hdPF+ro= github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= 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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 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-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 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.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 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/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8= github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= 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/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.3.0 h1:q4c+kbcR0d5rSurhBR8dIgieOaYpXtsdTYfx22Cu6rs= github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/zapr v0.2.0 h1:v6Ji8yBW77pva6NkJKQdHLAJKrIJKRHz0RXwPqCHSR4= github.com/go-logr/zapr v0.2.0/go.mod h1:qhKdvif7YF5GI9NWEpyxTSSBdGmzkNguibrdCNVPunU= 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.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= 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-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= 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-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/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/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1 h1:A8Yhf6EtqTv9RMsU6MQTyrtV1TjWlR6xU9BsZIwuTCM= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE= github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY= github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 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/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:euTFbi2YJgwcju3imEt919lhJKF68nN1cQPq3aA+kBE= github.com/hashicorp/vault/api v1.2.0 h1:ysGFc6XRGbv05NsWPzuO5VTv68Lj8jtwATxRLFOpP9s= github.com/hashicorp/vault/api v1.2.0/go.mod h1:dAjw0T5shMnrfH7Q/Mst+LrcTKvStZBVs1PICEDpUqY= github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= github.com/hashicorp/vault/sdk v0.2.1 h1:S4O6Iv/dyKlE9AUTXGa7VOvZmsCvg36toPKgV4f2P4M= github.com/hashicorp/vault/sdk v0.2.1/go.mod h1:WfUiO1vYzfBkz1TmoE4ZGU7HD0T0Cl/rZwaxjBkgN4U= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10 h1:6q5mVkdH/vYmqngx7kZQTjJ5HRsx+ImorDIEQ+beJgc= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 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.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 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/konsorten/go-windows-terminal-sequences v1.0.3/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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 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-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= 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/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 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.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 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.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 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/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 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.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 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_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 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 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 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.0.0-20181126121408-4724e9255275/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/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/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/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 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.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= 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-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 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 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 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/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 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/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 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/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/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-20190724013045-ca1201d0de80/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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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-20180823144017-11551d06cbcc/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-20181026203630-95b1ffbd15a5/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-20190129075346-302c3dd5f1cc/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/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-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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054 h1:HHeAlu5H9b71C+Fx0K+1dGgVFN1DM1/wz4aoGOA5qS8= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.1.0 h1:Phva6wqu+xR//Njw6iorylFFgn/z547tw5Ne3HZPQ+k= gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 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/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 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-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 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.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/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.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 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-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= k8s.io/api v0.20.2 h1:y/HR22XDZY3pniu9hIFDLpUCPq2w5eQ6aV/VFQ7uJMw= k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8= k8s.io/apiextensions-apiserver v0.20.1 h1:ZrXQeslal+6zKM/HjDXLzThlz/vPSxrfK3OqL8txgVQ= k8s.io/apiextensions-apiserver v0.20.1/go.mod h1:ntnrZV+6a3dB504qwC5PN/Yg9PBiDNt1EVqbW2kORVk= k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg= k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= k8s.io/client-go v0.20.2 h1:uuf+iIAbfnCSw8IGAv/Rg0giM+2bOzHLOsbbrwrdhNQ= k8s.io/client-go v0.20.2/go.mod h1:kH5brqWqp7HDxUFKoEgiI4v8G1xzbe9giaCenUWJzgE= k8s.io/code-generator v0.20.1/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.2 h1:LMmu5I0pLtwjpp5009KLuMGFqSc2S2isGw8t1hpYKLE= k8s.io/component-base v0.20.2/go.mod h1:pzFtCiwe/ASD0iV7ySMu8SYVJjCapNM9bjvk7ptpKh0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 h1:0T5IaWHO3sJTEmCP6mUlBvMukxPKUQWqiI/YuiBNMiQ= k8s.io/utils v0.0.0-20210111153108-fddb29f9d009/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/controller-runtime v0.8.3 h1:GMHvzjTmaWHQB8HadW+dIvBoJuLvZObYJ5YoZruPRao= sigs.k8s.io/controller-runtime v0.8.3/go.mod h1:U/l+DUopBc1ecfRZ5aviA9JDmGFQKvLf5YkZNx2e0sU= sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 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= ================================================ FILE: hack/boilerplate.go.txt ================================================ /* Copyright 2021. 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. */ ================================================ FILE: main.go ================================================ /* Copyright 2021. 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 main import ( "context" "flag" "fmt" "os" "strings" "time" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/healthz" //logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" secretsmanagerv1alpha1 "github.com/tuenti/secrets-manager/api/v1alpha1" "github.com/tuenti/secrets-manager/backend" "github.com/tuenti/secrets-manager/controllers" //+kubebuilder:scaffold:imports ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { corev1.AddToScheme(scheme) utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(secretsmanagerv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } // To be filled from build ldflags var version string func main() { var metricsAddr string var controllerName string var enableLeaderElection bool var enableDebugLog bool var versionFlag bool var reconcilePeriod time.Duration var selectedBackend string var watchNamespaces string var excludeNamespaces string var mgr ctrl.Manager var namespaceList []string backendCfg := backend.Config{} var probeAddr string flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&controllerName, "controller-name", "SecretDefinition", "If running secrets manager in multiple namespaces, set the controller name to something unique avoid 'duplicate metrics collector registration attempted' errors.") flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") flag.StringVar(&selectedBackend, "backend", "vault", "Selected backend. One of vault or azure-kv") flag.BoolVar(&enableDebugLog, "enable-debug-log", false, "Enable this to get more logs verbosity and debug messages.") flag.BoolVar(&versionFlag, "version", false, "Display Secret Manager version") flag.DurationVar(&reconcilePeriod, "reconcile-period", 5*time.Second, "How often the controller will re-queue secretdefinition events") flag.DurationVar(&backendCfg.BackendTimeout, "config.backend-timeout", 5*time.Second, "Backend connection timeout") flag.StringVar(&backendCfg.VaultURL, "vault.url", "https://127.0.0.1:8200", "Vault address. VAULT_ADDR environment would take precedence.") flag.StringVar(&backendCfg.VaultAuthMethod, "vault.auth-method", "approle", "Vault authentication method. Supported: approle, kubernetes.") flag.StringVar(&backendCfg.VaultRoleID, "vault.role-id", "", "Vault approle role id. VAULT_ROLE_ID environment would take precedence.") flag.StringVar(&backendCfg.VaultSecretID, "vault.secret-id", "", "Vault approle secret id. VAULT_SECRET_ID environment would take precedence.") flag.StringVar(&backendCfg.VaultKubernetesRole, "vault.kubernetes-role", "", "Vault kubernetes role name.") flag.Int64Var(&backendCfg.VaultMaxTokenTTL, "vault.max-token-ttl", 300, "Max seconds to consider a token expired.") flag.DurationVar(&backendCfg.VaultTokenPollingPeriod, "vault.token-polling-period", 15*time.Second, "Polling interval to check token expiration time.") flag.IntVar(&backendCfg.VaultRenewTTLIncrement, "vault.renew-ttl-increment", 600, "TTL time for renewed token.") flag.StringVar(&backendCfg.VaultEngine, "vault.engine", "kv2", "Vault secret engine. Only KV version 1 and 2 supported") flag.StringVar(&backendCfg.VaultApprolePath, "vault.approle-path", "approle", "Vault approle login path") flag.StringVar(&backendCfg.VaultKubernetesPath, "vault.kubernetes-path", "kubernetes", "Vault kubernetes login path") flag.StringVar(&backendCfg.AzureKVName, "azure-kv.name", "", "Azure KeyVault name. AZURE_KV_NAME environment would take precedence") flag.StringVar(&backendCfg.AzureKVTenantID, "azure-kv.tenant-id", "", "Azure KeyVault Tenant ID. AZURE_TENANT_ID environment would take precedence") flag.StringVar(&backendCfg.AzureKVClientID, "azure-kv.client-id", "", "Azure KeyVault ClientID used to authenticate. AZURE_CLIENT_ID environment would take precedence") flag.StringVar(&backendCfg.AzureKVClientSecret, "azure-kv.client-secret", "", "Azure KeyVault Client Secret used to authenticate. AZURE_CLIENT_SECRET environment would take precedence") flag.StringVar(&backendCfg.AzureKVManagedClientID, "azure-kv.managed-client-id", "", "Azure Managed Identity Client ID used to authenticate. AZURE_MANAGED_CLIENT_ID environment would take precedence") flag.StringVar(&backendCfg.AzureKVManagedResourceID, "azure-kv.managed-resource-id", "", "Azure Managed Identity Resource ID used to authenticate. AZURE_MANAGED_RESOURCE_ID environment would take precedence") flag.StringVar(&watchNamespaces, "watch-namespaces", "", "Comma separated list of namespaces that secrets-manager will watch for SecretDefinitions. By default all namespaces are watched.") flag.StringVar(&excludeNamespaces, "exclude-namespaces", "", "Comma separated list of namespaces that secrets-manager will not watch for SecretDefinitions. By default all namespaces are watched.") //New opts := zap.Options{ Development: enableDebugLog, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) backendLog := ctrl.Log.WithName("backend") if versionFlag { fmt.Printf("Secrets Manager %s\n", version) os.Exit(0) } if os.Getenv("VAULT_ADDR") != "" { backendCfg.VaultURL = os.Getenv("VAULT_ADDR") } if os.Getenv("VAULT_ROLE_ID") != "" { backendCfg.VaultRoleID = os.Getenv("VAULT_ROLE_ID") } if os.Getenv("VAULT_SECRET_ID") != "" { backendCfg.VaultSecretID = os.Getenv("VAULT_SECRET_ID") } if os.Getenv("AZURE_KV_NAME") != "" { backendCfg.AzureKVName = os.Getenv("AZURE_KV_NAME") } // Capture Azure SP Env auth method environment variables // This will let inner code to authenticate using a mix of env and args if os.Getenv("AZURE_TENANT_ID") != "" { backendCfg.AzureKVTenantID = os.Getenv("AZURE_TENANT_ID") } if os.Getenv("AZURE_CLIENT_ID") != "" { backendCfg.AzureKVClientID = os.Getenv("AZURE_CLIENT_ID") } if os.Getenv("AZURE_CLIENT_SECRET") != "" { backendCfg.AzureKVClientSecret = os.Getenv("AZURE_CLIENT_SECRET") } if os.Getenv("AZURE_MANAGED_CLIENT_ID") != "" { backendCfg.AzureKVManagedClientID = os.Getenv("AZURE_MANAGED_CLIENT_ID") } if os.Getenv("AZURE_MANAGED_RESOURCE_ID") != "" { backendCfg.AzureKVManagedResourceID = os.Getenv("AZURE_MANAGED_RESOURCE_ID") } ctx, cancel := context.WithCancel(context.Background()) defer cancel() backendClient, err := backend.NewBackendClient(ctx, selectedBackend, backendLog, backendCfg) if err != nil { setupLog.Error(err, "could not build backend client") os.Exit(1) } nsSlice := func(ns string) []string { trimmed := strings.Trim(strings.TrimSpace(ns), "\"") return strings.Split(trimmed, ",") } excludeNs := make(map[string]bool) if len(excludeNamespaces) > 0 { for _, ns := range nsSlice(excludeNamespaces) { excludeNs[ns] = true } } if len(strings.TrimSpace(watchNamespaces)) > 0 { setupLog.Info("setting restricted namespace list for controller") namespaceList = nsSlice(watchNamespaces) setupLog.Info("watching namespaces: " + watchNamespaces) mgr, err = ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "5ac9a181.secrets-manager.tuenti.io", NewCache: cache.MultiNamespacedCacheBuilder(namespaceList), }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } } else { mgr, err = ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "5ac9a181.secrets-manager.tuenti.io", }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } } if err = (&controllers.SecretDefinitionReconciler{ Client: mgr.GetClient(), Backend: *backendClient, APIReader: mgr.GetAPIReader(), Log: ctrl.Log.WithName("controllers").WithName("SecretDefinition"), ReconciliationPeriod: reconcilePeriod, ExcludeNamespaces: excludeNs, }).SetupWithManager(mgr, controllerName); err != nil { setupLog.Error(err, "unable to create controller", "controller", "SecretDefinition") os.Exit(1) } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up ready check") os.Exit(1) } setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } } ================================================ FILE: scripts/setup-dev-env.sh ================================================ #!/bin/bash # Download tools go get -v github.com/golang/mock/gomock go get -v github.com/golang/mock/mockgen go get -v github.com/Masterminds/glide # Install mockgen go install github.com/golang/mock/mockgen