Showing preview only (299K chars total). Download the full file or copy to clipboard to get everything.
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 (`<major>.<minor>`, 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<target>\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
[](https://circleci.com/gh/tuenti/secrets-manager/tree/master)
[](https://goreportcard.com/report/github.com/tuenti/secrets-manager)
[](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 <<EOF
---
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
EOF
```
To deploy it just run `kubectl apply -f secretdefinition-sample.yaml`
## Flags
| Flag | Default | Description |
| ------ | ------- | ------ |
| `backend`| vault | Selected backend. One of vault or azure-kv |
| `enable-debug-log` | `false` | Enable this to get more logs verbosity and debug messages.|
| `enable-leader-election` | `false` | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.|
| `reconcile-period`| 5s | How often the controller will re-queue secretdefinition events |
| `config.backend-timeout`| 5s | Backend connection timeout |
| `azure-kv.name` | `""` | Azure KeyVault name. `AZURE_KV_NAME` environment would take precedence |
| `azure-kv.tenant-id` | `""` | Azure KeyVault Tenant ID. `AZURE_TENANT_ID` environment would take precedence |
| `azure-kv.client-id` | `""` | Azure KeyVault Cliend ID used to authenticate. `AZURE_CLIENT_ID` environment would take precedence |
| `azure-kv.client-secret` | `""` | Azure KeyVault Client Secret used to authenticate. `AZURE_CLIENT_SECRET` environment would take precedence |
| `azure-kv.managed-client-id` | `""` | Azure Managed Identity Client ID used to authenticate. `AZURE_MANAGED_CLIENT_ID` environment would take precedence |
| `azure-kv.managed-resource-id` | `""` | Azure Managed Identity Resource ID used to authenticate. `AZURE_MANAGED_RESOURCE_ID` environment would take precedence |
| `vault.url` | https://127.0.0.1:8200 | Vault address. `VAULT_ADDR` environment would take precedence. |
| `vault.role-id` | `""` | Vault appRole `role_id`. `VAULT_ROLE_ID` environment would take precedence. |
| `vault.secret-id` | `""` | Vault appRole `secret_id`. `VAULT_SECRET_ID` environment would take precedence. |
| `vault.engine` | kv2 | Vault secrets engine to use. Only key/value engines supported. Default is kv version 2 |
| `vault.auth-method` | approle | Vault authentication method. Supported: approle, kubernetes. |
| `vault.approle-path` | approle | Vault approle login path |
| `vault.kubernetes-path` | kubernetes | Vault kubernetes login path |
| `vault.kubernetes-role` | `""` | Vault kubernetes role name |
| `vault.max-token-ttl` | 300 |Max seconds to consider a token expired. |
| `vault.token-polling-period` | 15s | Polling interval to check token expiration time. |
| `vault.renew-ttl-increment` | 600 | TTL time for renewed token. |
| `metrics-addr` | `:8080` | The address to listen on for HTTP requests. |
| `controller-name` | SecretDefinition | If running secrets manager in multiple namespaces, set the controller name to something unique avoid 'duplicate metrics collector registration attempted' errors. |
| `watch-namespaces` | `""` | Comma separated list of namespaces that secrets-manager will watch for `SecretDefinitions`. By default all namespaces are watched. |
| `exclude-namespaces` | `""` | Comma separated list of namespaces that secrets-manager will not watch for `SecretDefinitions`. By default all namespaces are watched. Note that if you exclude and watch the same namespace, excluding it will be prioritized. |
## RBAC
Secrets Manager can be run in one of 2 ways:
* Global secrets management in all namespaces for the whole of a Kuberentes cluster
* Manage specific namespaces
In order for Secrets Manager to act as a manager for all Namespaces it requires a ClusterRole that enables it to manage all secrets and secretdefinitions in the entire Kubernetes cluster as in the [config/rbac/role.yaml](config/rbac/role.yaml) and [config/rbac/rolebinding.yaml](config/rbac/rolebinding.yaml) examples.
Alternatively if you use the `watch-namespaces` argument to limit secretdefinition monitoring to sepcific namespaces then you can just give the `serviceAccount` that `secrets-manager` is running as a standard role and a rolebinding in each of the namespaces that you want it to manage as shown in the [config/rbac/secrets_manager_role.yaml](config/rbac/secrets_manager_role.yaml) and [config/rbac/secrets_manager_role_binding.yaml](config/rbac/secrets_manager_role_binding.yaml) examples. Alternatively you can still use a cluster role if you so wish.
To be able to interact with `secretdefinition` resources using the standard `admin`, `edit` and `view` Kubernetes native roles, you need to create the following `ClusterRole` aggregations:
- [config/rbac/secretdefinitions_admin_clusterrole_aggregation.yaml](config/rbac/secretdefinitions_admin_clusterrole_aggregation.yaml)
- [config/rbac/secretdefinitions_view_clusterrole_aggregation.yaml](config/rbac/secretdefinitions_view_clusterrole_aggregation.yaml)
More information about aggregated `ClusterRoles` can be found at [kubernetes.io > 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 <<EOF
path "secret/data/my-k8s-cluster/*" {
capabilities = ["read"]
}
EOF
$ cat my-policy.hcl | vault policy write my-policy -
```
### Vault Tokens
Vault tokens will be renewed by `secrets-manager` if the `ttl` is lower than `vault.max-token-ttl` and the token is renewable. But as per Vault's [documentation](https://www.vaultproject.io/docs/concepts/tokens.html#the-general-case), regular tokens will have their own max TTL that it's calculated on every renewal, so that a token will eventually expire. This can be ok for your use case, but for others a [periodic token](https://www.vaultproject.io/docs/concepts/tokens.html#periodic-tokens) could be much more convinient. In the case of a periodic token, the `period` will invalidate the `vault.renew-ttl-increment` option.
### Vault AppRole
Vault token as a login mechanism has been deprecated in favor of the [AppRole](https://www.vaultproject.io/docs/auth/approle.html) authentication method for `secrets-manager`.
`secrets-manager` will still renew the token obtained after login in, but will make `secrets-manager` more resilient in case of a token has expired due to network issues, Vault sealed, etc.
So instead of expecting a token, `secrets-manager` expects a `role_id` and a `secret_id` to connect to Vault.
To create a role with a permanent `secret_id` attached to a policy:
`$ vault write auth/approle/role/secrets-manager policies=my-policy secret_id_num_uses=0 secret_id_ttl=0`
To get a `secret_id`:
`$ vault write -force auth/approle/role/secrets-manager/secret-id`
To get the `role_id`:
`$ vault read auth/approle/role/secrets-manager/role-id`
### Vault Kubernetes Authentication
In addition to `appRole`, `secrets-manager` can authenticate to Vault using its own Kubernetes `serviceAccount`. Follow the [Vault Kubernetes auth guide](https://www.vaultproject.io/docs/auth/kubernetes) to enable it and configure it.
Example:
```sh
$ cat > secrets-manager-role.json <<EOF
{
"bound_service_account_names": [ "secrets-manager" ],
"bound_service_account_namespaces": [ "my-namesapace" ],
"policies": [ "my-policy" ],
"max_ttl": 3600
}
EOF
$ vault write auth/kubernetes/role/secrets-manager @secrets-manager-role.json
```
## Getting Started with Azure KeyVault
### Deploy Azure KeyVault
If you haven't still deployed an Azure KeyVault server, you can do it with Azure CLI:
```
$ az keyvault create --location <location> --name <keyvault_name> --resource-group <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 "<service_principal_name>" --role Contributor --scopes /subscriptions/{SubID}/resourceGroups/{ResourceGroup}
```
This command will output a JSON object like the following:
```
{
"appId": "<AppID>", // ClientId
"displayName": "<ServicePrincipleName>",
"name": "http://<ServicePrincipleName>",
"password": "<Password>", // ClientSecret
"tenant": "<TenantId>"
}
```
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 <keyvault_name> --spn <appId> --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=<TOKEN_FROM_STEP_3> ./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 <<EOF
path "secret/data/*" {
capabilities = ["read"]
}
EOF
cat secrets-manager.hcl | vault policy write secrets-manager -
echo "creating role"
vault write auth/approle/role/secrets-manager policies=secrets-manager secret_id_num_uses=0 secret_id_ttl=0
echo "creating some secrets"
vault kv put secret/pathtosecret1 "value=dmFsdWUzCg=="
vault kv put secret/pathtosecret2 "value=value2"
vault kv put secret/pathtosecret3 "value=value3"
echo "creating approle secret"
kubectl delete secret vault-approle-secret 2>/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:KmX6BPdI08NWT
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
SYMBOL INDEX (230 symbols across 25 files)
FILE: api/v1alpha1/groupversion_info.go
constant Group (line 28) | Group = "secrets-manager.tuenti.io"
constant Version (line 29) | Version = "v1alpha1"
FILE: api/v1alpha1/secretdefinition_types.go
type DataSource (line 23) | type DataSource struct
type SecretDefinitionSpec (line 33) | type SecretDefinitionSpec struct
type SecretDefinitionStatus (line 42) | type SecretDefinitionStatus struct
type SecretDefinition (line 50) | type SecretDefinition struct
type SecretDefinitionList (line 61) | type SecretDefinitionList struct
function init (line 67) | func init() {
FILE: api/v1alpha1/suite_test.go
function TestAPIs (line 41) | func TestAPIs(t *testing.T) {
FILE: api/v1alpha1/zz_generated.deepcopy.go
method DeepCopyInto (line 29) | func (in *DataSource) DeepCopyInto(out *DataSource) {
method DeepCopy (line 34) | func (in *DataSource) DeepCopy() *DataSource {
method DeepCopyInto (line 44) | func (in *SecretDefinition) DeepCopyInto(out *SecretDefinition) {
method DeepCopy (line 53) | func (in *SecretDefinition) DeepCopy() *SecretDefinition {
method DeepCopyObject (line 63) | func (in *SecretDefinition) DeepCopyObject() runtime.Object {
method DeepCopyInto (line 71) | func (in *SecretDefinitionList) DeepCopyInto(out *SecretDefinitionList) {
method DeepCopy (line 85) | func (in *SecretDefinitionList) DeepCopy() *SecretDefinitionList {
method DeepCopyObject (line 95) | func (in *SecretDefinitionList) DeepCopyObject() runtime.Object {
method DeepCopyInto (line 103) | func (in *SecretDefinitionSpec) DeepCopyInto(out *SecretDefinitionSpec) {
method DeepCopy (line 115) | func (in *SecretDefinitionSpec) DeepCopy() *SecretDefinitionSpec {
method DeepCopyInto (line 125) | func (in *SecretDefinitionStatus) DeepCopyInto(out *SecretDefinitionStat...
method DeepCopy (line 130) | func (in *SecretDefinitionStatus) DeepCopy() *SecretDefinitionStatus {
FILE: backend/azure_kv.go
constant azureKVEndpoint (line 18) | azureKVEndpoint = "vault.azure.net"
type azureKVClient (line 21) | type azureKVClient struct
method ReadSecret (line 86) | func (c *azureKVClient) ReadSecret(path string, key string) (string, e...
function getAzureCredential (line 29) | func getAzureCredential(ctx context.Context, logger logr.Logger, cfg Con...
function azureKeyVaultClient (line 54) | func azureKeyVaultClient(ctx context.Context, l logr.Logger, cfg Config)...
FILE: backend/azure_kv_metrics.go
type azureKVMetrics (line 24) | type azureKVMetrics struct
method updateSecretReadErrorsTotalMetric (line 36) | func (vm *azureKVMetrics) updateSecretReadErrorsTotalMetric(path strin...
method updateLoginErrorsTotalMetric (line 46) | func (vm *azureKVMetrics) updateLoginErrorsTotalMetric() {
function newAzureKVMetrics (line 28) | func newAzureKVMetrics(keyvaultName string, tenantID string) *azureKVMet...
FILE: backend/azure_kv_metrics_test.go
function TestAzureKVUpdateLoginErrorsTotal (line 11) | func TestAzureKVUpdateLoginErrorsTotal(t *testing.T) {
function TestAzureKVUpdateReadSecretErrorsTotal (line 20) | func TestAzureKVUpdateReadSecretErrorsTotal(t *testing.T) {
FILE: backend/azure_kv_test.go
constant fakeKeyVaultName (line 21) | fakeKeyVaultName = "azure-keyvault-fake-name"
constant fakeKeyVaultTenant (line 22) | fakeKeyVaultTenant = "01234567-0123-0123-0123-0123456789ab"
constant fakeKeyVaultSecret (line 23) | fakeKeyVaultSecret = "fake-secret"
function akvGetSecret (line 36) | func akvGetSecret(w http.ResponseWriter, r *http.Request) {
type FakeCredential (line 88) | type FakeCredential struct
method GetToken (line 100) | func (f *FakeCredential) GetToken(ctx context.Context, options policy....
function NewFakeCredential (line 93) | func NewFakeCredential(accountName, accountKey string) *FakeCredential {
function TestGetAzureCredential (line 107) | func TestGetAzureCredential(t *testing.T) {
function TestAzureKeyVaultClient (line 200) | func TestAzureKeyVaultClient(t *testing.T) {
function TestAzureKVClientReadSecret (line 232) | func TestAzureKVClientReadSecret(t *testing.T) {
FILE: backend/backend.go
constant vaultBackendName (line 12) | vaultBackendName = "vault"
constant azureKVBackendName (line 13) | azureKVBackendName = "azure-kv"
function init (line 18) | func init() {
type Config (line 26) | type Config struct
type Client (line 48) | type Client interface
function NewBackendClient (line 53) | func NewBackendClient(ctx context.Context, backend string, logger logr.L...
FILE: backend/backend_test.go
function TestNotImplementedBackend (line 25) | func TestNotImplementedBackend(t *testing.T) {
function TestMain (line 34) | func TestMain(m *testing.M) {
FILE: backend/decoder.go
constant Base64EncodingType (line 11) | Base64EncodingType = "base64"
constant TextEncodingType (line 14) | TextEncodingType = "text"
constant DefaultEncodingType (line 17) | DefaultEncodingType = "text"
type Decoder (line 21) | type Decoder interface
type Base64Decoder (line 26) | type Base64Decoder struct
method DecodeString (line 36) | func (d Base64Decoder) DecodeString(input string) ([]byte, error) {
type TextDecoder (line 31) | type TextDecoder struct
method DecodeString (line 45) | func (d TextDecoder) DecodeString(input string) ([]byte, error) {
function NewDecoder (line 50) | func NewDecoder(encoding string) (Decoder, error) {
FILE: backend/decoder_test.go
function TestNotImplementedDecoder (line 11) | func TestNotImplementedDecoder(t *testing.T) {
function TestGetB64Decoder (line 17) | func TestGetB64Decoder(t *testing.T) {
function TestGetTextDecoder (line 25) | func TestGetTextDecoder(t *testing.T) {
function TestGetTextDecoderFromEmptyString (line 33) | func TestGetTextDecoderFromEmptyString(t *testing.T) {
function TestDecodeB64String (line 41) | func TestDecodeB64String(t *testing.T) {
function TestDecodeInvalidB64String (line 49) | func TestDecodeInvalidB64String(t *testing.T) {
function TestDecodeText (line 57) | func TestDecodeText(t *testing.T) {
FILE: backend/vault.go
constant defaultSecretKey (line 22) | defaultSecretKey = "data"
constant kubernetesJwtTokenPath (line 23) | kubernetesJwtTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/...
constant kubernetesAuthMethod (line 24) | kubernetesAuthMethod = "kubernetes"
constant appRoleAuthMethod (line 25) | appRoleAuthMethod = "approle"
type client (line 28) | type client struct
method vaultLogin (line 44) | func (c *client) vaultLogin() error {
method vaultAppRoleLogin (line 60) | func (c *client) vaultAppRoleLogin() error {
method vaultKubernetesLogin (line 73) | func (c *client) vaultKubernetesLogin(podSATokenReader io.Reader) error {
method getToken (line 161) | func (c *client) getToken() (*api.Secret, error) {
method getTokenTTL (line 171) | func (c *client) getTokenTTL(token *api.Secret) (int64, error) {
method renewToken (line 181) | func (c *client) renewToken(token *api.Secret) error {
method renewalLoop (line 200) | func (c *client) renewalLoop() {
method startTokenRenewer (line 229) | func (c *client) startTokenRenewer(ctx context.Context) {
method ReadSecret (line 244) | func (c *client) ReadSecret(path string, key string) (string, error) {
function vaultClient (line 90) | func vaultClient(l logr.Logger, cfg Config) (*client, error) {
FILE: backend/vault_engine.go
constant kvEngineV1Name (line 9) | kvEngineV1Name = "kv1"
constant kvEngineV2Name (line 10) | kvEngineV2Name = "kv2"
type engine (line 13) | type engine interface
type kvEngineV1 (line 17) | type kvEngineV1 struct
method getData (line 25) | func (e kvEngineV1) getData(s *api.Secret) map[string]interface{} {
type kvEngineV2 (line 21) | type kvEngineV2 struct
method getData (line 29) | func (e kvEngineV2) getData(s *api.Secret) map[string]interface{} {
function newEngine (line 36) | func newEngine(eng string) (engine, error) {
FILE: backend/vault_engine_test.go
function TestNewEngineKV1 (line 12) | func TestNewEngineKV1(t *testing.T) {
function TestNewEngineKV2 (line 19) | func TestNewEngineKV2(t *testing.T) {
function TestNotImplementedEngine (line 26) | func TestNotImplementedEngine(t *testing.T) {
function TestGetDataKv1 (line 33) | func TestGetDataKv1(t *testing.T) {
function TestGetDataKv2 (line 43) | func TestGetDataKv2(t *testing.T) {
function TestGetDataKv2WithKv1Engine (line 55) | func TestGetDataKv2WithKv1Engine(t *testing.T) {
FILE: backend/vault_metrics.go
constant vaultLookupSelfOperationName (line 10) | vaultLookupSelfOperationName = "lookup-self"
constant vaultRenewSelfOperationName (line 11) | vaultRenewSelfOperationName = "renew-self"
constant vaultIsRenewableOperationName (line 12) | vaultIsRenewableOperationName = "is-renewable"
type vaultMetrics (line 53) | type vaultMetrics struct
method updateVaultMaxTokenTTLMetric (line 77) | func (vm *vaultMetrics) updateVaultMaxTokenTTLMetric(value int64) {
method updateVaultTokenTTLMetric (line 86) | func (vm *vaultMetrics) updateVaultTokenTTLMetric(value int64) {
method updateVaultSecretReadErrorsTotalMetric (line 95) | func (vm *vaultMetrics) updateVaultSecretReadErrorsTotalMetric(path st...
method updateVaultTokenRenewalErrorsTotalMetric (line 107) | func (vm *vaultMetrics) updateVaultTokenRenewalErrorsTotalMetric(vault...
method updateVaultLoginErrorsTotalMetric (line 118) | func (vm *vaultMetrics) updateVaultLoginErrorsTotalMetric() {
function init (line 57) | func init() {
function newVaultMetrics (line 66) | func newVaultMetrics(vaultAddr string, vaultVersion string, vaultEngine ...
FILE: backend/vault_metrics_test.go
constant fakeVaultAddress (line 12) | fakeVaultAddress = "https://vault.example.com:8200"
constant fakeVaultVersion (line 13) | fakeVaultVersion = "0.11.1"
constant fakeVaultEngine (line 14) | fakeVaultEngine = "kv2"
constant fakeVaultClusterID (line 15) | fakeVaultClusterID = "vault-fake-1"
constant fakeVaultClusterName (line 16) | fakeVaultClusterName = "vault-fake"
function TestUpdateMaxTokenTTL (line 19) | func TestUpdateMaxTokenTTL(t *testing.T) {
function TestUpdateTokenTTL (line 28) | func TestUpdateTokenTTL(t *testing.T) {
function TestUpdateTokenLookupErrorsTotal (line 37) | func TestUpdateTokenLookupErrorsTotal(t *testing.T) {
function TestUpdateTokenRenewErrorsTotal (line 46) | func TestUpdateTokenRenewErrorsTotal(t *testing.T) {
function TestUpdateReadSecretErrorsTotal (line 61) | func TestUpdateReadSecretErrorsTotal(t *testing.T) {
FILE: backend/vault_test.go
constant vaultAPIVersion (line 19) | vaultAPIVersion = "v1"
constant vaultFakeClusterName (line 20) | vaultFakeClusterName = "vault-mock-cluster"
constant vaultFakeClusterID (line 21) | vaultFakeClusterID = "vault-mock-cluster-1"
constant vaultFakeVersion (line 22) | vaultFakeVersion = "0.11.1"
constant selectedBackend (line 23) | selectedBackend = "vault"
constant fakeToken (line 24) | fakeToken = "fake-token"
constant vaultFakeRoleID (line 25) | vaultFakeRoleID = "12345678-9aaa-bbbb-cccc-dddddddddddd"
constant vaultFakeSecretID (line 26) | vaultFakeSecretID = "eeeeeeee-ffff-0000-1111-123456789aaa"
constant vaultAppRolePath (line 27) | vaultAppRolePath = "approle"
constant defaultTokenTTL (line 28) | defaultTokenTTL = 40
constant defaultTokenRenewable (line 29) | defaultTokenRenewable = true
constant defaultRevokedToken (line 30) | defaultRevokedToken = false
constant defaultInvalidAppRole (line 31) | defaultInvalidAppRole = false
constant defaultKubernetesRole (line 32) | defaultKubernetesRole = false
constant fakeKubernetesSAToken (line 33) | fakeKubernetesSAToken = `eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOi...
type testConfig (line 36) | type testConfig struct
function v1SysHealth (line 49) | func v1SysHealth(w http.ResponseWriter, r *http.Request) {
function v1AuthTokenLookupSelf (line 73) | func v1AuthTokenLookupSelf(w http.ResponseWriter, r *http.Request) {
function v1AuthTokenRenewSelf (line 120) | func v1AuthTokenRenewSelf(w http.ResponseWriter, r *http.Request) {
function v1AuthKubernetesLogin (line 160) | func v1AuthKubernetesLogin(w http.ResponseWriter, r *http.Request) {
function v1AuthAppRoleLogin (line 193) | func v1AuthAppRoleLogin(w http.ResponseWriter, r *http.Request) {
function v1SecretTestKv2 (line 243) | func v1SecretTestKv2(w http.ResponseWriter, r *http.Request) {
function v1SecretTestKv1 (line 274) | func v1SecretTestKv1(w http.ResponseWriter, r *http.Request) {
function TestVaultLoginKubernetes (line 297) | func TestVaultLoginKubernetes(t *testing.T) {
function TestVaultBackendInvalidCfg (line 316) | func TestVaultBackendInvalidCfg(t *testing.T) {
function TestVaultBackend (line 326) | func TestVaultBackend(t *testing.T) {
function TestVaultLoginInvalidRoleId (line 334) | func TestVaultLoginInvalidRoleId(t *testing.T) {
function TestVaultLoginInvalidSecretId (line 344) | func TestVaultLoginInvalidSecretId(t *testing.T) {
function TestVaultClient (line 354) | func TestVaultClient(t *testing.T) {
function TestVaultClientInvalidCfg (line 363) | func TestVaultClientInvalidCfg(t *testing.T) {
function TestGetToken (line 370) | func TestGetToken(t *testing.T) {
function TestGetTokenTTL (line 377) | func TestGetTokenTTL(t *testing.T) {
function TestRenewToken (line 390) | func TestRenewToken(t *testing.T) {
function TestRenewTokenRevokedToken (line 404) | func TestRenewTokenRevokedToken(t *testing.T) {
function TestTokenNotRenewableError (line 421) | func TestTokenNotRenewableError(t *testing.T) {
function TestRenewalLoopRevokedToken (line 440) | func TestRenewalLoopRevokedToken(t *testing.T) {
function TestRenewalLoopNotRenewableToken (line 452) | func TestRenewalLoopNotRenewableToken(t *testing.T) {
function TestRenewalLoopInvalidRoleId (line 468) | func TestRenewalLoopInvalidRoleId(t *testing.T) {
function TestRenewalLoopInvalidSecretId (line 485) | func TestRenewalLoopInvalidSecretId(t *testing.T) {
function TestReadSecretKv2 (line 502) | func TestReadSecretKv2(t *testing.T) {
function TestReadSecretKv1 (line 509) | func TestReadSecretKv1(t *testing.T) {
function TestSecretNotFound (line 519) | func TestSecretNotFound(t *testing.T) {
FILE: controllers/metrics.go
function init (line 34) | func init() {
FILE: controllers/secretdefinition_controller.go
constant timestampFormat (line 38) | timestampFormat = "2006-01-02T15.04.05Z"
constant finalizerName (line 39) | finalizerName = "secret.finalizer." + smv1alpha1.Group
constant managedByLabel (line 40) | managedByLabel = "app.kubernetes.io/managed-by"
constant lastUpdateLabel (line 41) | lastUpdateLabel = smv1alpha1.Group + "/lastUpdateTime"
type SecretDefinitionReconciler (line 45) | type SecretDefinitionReconciler struct
method getDesiredState (line 121) | func (r *SecretDefinitionReconciler) getDesiredState(keysMap map[strin...
method getCurrentState (line 146) | func (r *SecretDefinitionReconciler) getCurrentState(ctx context.Conte...
method upsertSecret (line 164) | func (r *SecretDefinitionReconciler) upsertSecret(ctx context.Context,...
method deleteSecret (line 174) | func (r *SecretDefinitionReconciler) deleteSecret(ctx context.Context,...
method shouldExclude (line 185) | func (r *SecretDefinitionReconciler) shouldExclude(sDefNamespace strin...
method AddFinalizerIfNotPresent (line 193) | func (r *SecretDefinitionReconciler) AddFinalizerIfNotPresent(ctx cont...
method Reconcile (line 235) | func (r *SecretDefinitionReconciler) Reconcile(ctx context.Context, re...
method SetupWithManager (line 318) | func (r *SecretDefinitionReconciler) SetupWithManager(mgr ctrl.Manager...
type skipfn (line 59) | type skipfn
function noSkip (line 61) | func noSkip(_ string) bool {
function skipAnnotation (line 65) | func skipAnnotation(key string) bool {
function mergeMap (line 69) | func mergeMap(dst map[string]string, srcMap map[string]string, skipKey s...
function getSecretFromSecretDefinition (line 78) | func getSecretFromSecretDefinition(sDef *smv1alpha1.SecretDefinition, da...
function containsString (line 88) | func containsString(slice []string, s string) bool {
function removeString (line 97) | func removeString(slice []string, s string) (result []string) {
function ignoreNotFoundError (line 108) | func ignoreNotFoundError(err error) error {
function isNotMarkedForRemoval (line 116) | func isNotMarkedForRemoval(sDef smv1alpha1.SecretDefinition) bool {
function getObjectMetaFromSecretDefinition (line 202) | func getObjectMetaFromSecretDefinition(sDef *smv1alpha1.SecretDefinition...
function init (line 325) | func init() {
FILE: controllers/secretdefinition_controller_test.go
constant encodedValue (line 25) | encodedValue = "bG9yZW0gaXBzdW0gZG9ybWEK"
constant decodedValue (line 26) | decodedValue = "lorem ipsum dorma"
FILE: controllers/suite_test.go
type fakeBackendSecret (line 53) | type fakeBackendSecret struct
type fakeBackend (line 59) | type fakeBackend struct
method ReadSecret (line 69) | func (f fakeBackend) ReadSecret(path string, key string) (string, erro...
function newFakeBackend (line 63) | func newFakeBackend(fakeSecrets []fakeBackendSecret) fakeBackend {
function getReconciler (line 79) | func getReconciler() *SecretDefinitionReconciler {
function getConfig (line 83) | func getConfig() *rest.Config {
function getScheme (line 87) | func getScheme() *runtime.Scheme {
function TestSecretDefinitionController (line 91) | func TestSecretDefinitionController(t *testing.T) {
FILE: errors/errors.go
constant UnknownErrorType (line 7) | UnknownErrorType = "UnknownError"
constant BackendNotImplementedErrorType (line 8) | BackendNotImplementedErrorType = "BackendNotImplementedError"
constant BackendSecretNotFoundErrorType (line 9) | BackendSecretNotFoundErrorType = "BackendSecretNotFoundError"
constant BackendSecretForbiddenErrorType (line 10) | BackendSecretForbiddenErrorType = "BackendSecretForbiddenError"
constant K8sSecretNotFoundErrorType (line 11) | K8sSecretNotFoundErrorType = "K8sSecretNotFoundError"
constant EncodingNotImplementedErrorType (line 12) | EncodingNotImplementedErrorType = "EncodingNotImplementedError"
constant VaultEngineNotImplementedErrorType (line 13) | VaultEngineNotImplementedErrorType = "VaultEngineNotImplementedError"
constant VaultTokenNotRenewableErrorType (line 14) | VaultTokenNotRenewableErrorType = "VaultTokenNotRenewableError"
type BackendNotImplementedError (line 18) | type BackendNotImplementedError struct
method Error (line 73) | func (e BackendNotImplementedError) Error() string {
type BackendSecretNotFoundError (line 24) | type BackendSecretNotFoundError struct
method Error (line 77) | func (e BackendSecretNotFoundError) Error() string {
type K8sSecretNotFoundError (line 31) | type K8sSecretNotFoundError struct
method Error (line 81) | func (e K8sSecretNotFoundError) Error() string {
type EncodingNotImplementedError (line 38) | type EncodingNotImplementedError struct
method Error (line 85) | func (e EncodingNotImplementedError) Error() string {
type VaultEngineNotImplementedError (line 44) | type VaultEngineNotImplementedError struct
method Error (line 89) | func (e VaultEngineNotImplementedError) Error() string {
type VaultTokenNotRenewableError (line 50) | type VaultTokenNotRenewableError struct
method Error (line 93) | func (e VaultTokenNotRenewableError) Error() string {
function getErrorType (line 54) | func getErrorType(err error) string {
function IsBackendNotImplemented (line 98) | func IsBackendNotImplemented(err error) bool {
function IsBackendSecretNotFound (line 103) | func IsBackendSecretNotFound(err error) bool {
function IsK8sSecretNotFound (line 108) | func IsK8sSecretNotFound(err error) bool {
function IsEncodingNotImplemented (line 113) | func IsEncodingNotImplemented(err error) bool {
function IsVaultEngineNotImplemented (line 118) | func IsVaultEngineNotImplemented(err error) bool {
function IsVaultTokenNotRenewable (line 123) | func IsVaultTokenNotRenewable(err error) bool {
FILE: errors/errors_test.go
function TestErrorString (line 11) | func TestErrorString(t *testing.T) {
function TestGetErrorType (line 26) | func TestGetErrorType(t *testing.T) {
function TestIsBackendNotImplemented (line 43) | func TestIsBackendNotImplemented(t *testing.T) {
function TestIsBackendSecretNotFound (line 50) | func TestIsBackendSecretNotFound(t *testing.T) {
function TestIsK8sSecretNotFound (line 57) | func TestIsK8sSecretNotFound(t *testing.T) {
function TestIsEncodingNotImplemented (line 64) | func TestIsEncodingNotImplemented(t *testing.T) {
function TestIsVaultEngineNotImplemented (line 71) | func TestIsVaultEngineNotImplemented(t *testing.T) {
function TestIsVaultTokenNotRenewable (line 76) | func TestIsVaultTokenNotRenewable(t *testing.T) {
FILE: main.go
function init (line 52) | func init() {
function main (line 62) | func main() {
Condensed preview — 77 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (303K chars).
[
{
"path": ".circleci/config.yml",
"chars": 1243,
"preview": "---\n version: 2.0\n\n jobs:\n unit_tests:\n docker:\n - image: circleci/golang:1.16\n environment:\n "
},
{
"path": ".dockerignore",
"chars": 41,
"preview": "vendor/\nbuild/\nDockerfile\n.git\n.gitignore"
},
{
"path": ".github/pull_request_template.md",
"chars": 1292,
"preview": "# Status\n\nREADY/IN DEVELOPMENT/HOLD\n\n# Migrations\n\nYES (describe migration) | NO\n\n# Description\n\nA few sentences describ"
},
{
"path": ".gitignore",
"chars": 350,
"preview": "\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\nbin\ntestbin\n\n# Tarballs\n*.tar*\n# Test binary, build"
},
{
"path": "CHANGELOG.md",
"chars": 5770,
"preview": "## Unreleased\n\n- [FEATURE] Add support for Azure KeyVault backend\n\n## v2.0.1 2022-04-04\n\n- [BUG] Fix nil pointer derefer"
},
{
"path": "CONTRIBUTING.md",
"chars": 1766,
"preview": "# Contributing to secrets-manager\n\nIf you find something missing or not working as expected, we are happy to receive you"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile",
"chars": 5176,
"preview": "\nDOCKER_REGISTRY = \"registry.hub.docker.com\"\nORGANIZATION = \"tuentitech\"\nBINARY_NAME=secrets-manager\nVERSION=$(shell dep"
},
{
"path": "PROJECT",
"chars": 384,
"preview": "domain: secrets-manager.tuenti.io\nlayout:\n- go.kubebuilder.io/v3\nprojectName: secrets-manager\nrepo: github.com/tuenti/se"
},
{
"path": "README.md",
"chars": 16266,
"preview": "# secrets-manager\n[](https://circle"
},
{
"path": "api/v1alpha1/groupversion_info.go",
"chars": 1298,
"preview": "/*\nCopyright 2021.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in "
},
{
"path": "api/v1alpha1/secretdefinition_types.go",
"chars": 2260,
"preview": "/*\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with "
},
{
"path": "api/v1alpha1/secretdefinition_types_test.go",
"chars": 2370,
"preview": "package v1alpha1\n\n/*\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except i"
},
{
"path": "api/v1alpha1/suite_test.go",
"chars": 2071,
"preview": "/*\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with "
},
{
"path": "api/v1alpha1/zz_generated.deepcopy.go",
"chars": 4096,
"preview": "//go:build !ignore_autogenerated\n// +build !ignore_autogenerated\n\n/*\nCopyright 2021.\n\nLicensed under the Apache License,"
},
{
"path": "backend/azure_kv.go",
"chars": 3187,
"preview": "package backend\n\nimport (\n\t\"context\"\n\tgoerrors \"errors\"\n\t\"fmt\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github"
},
{
"path": "backend/azure_kv_metrics.go",
"chars": 1468,
"preview": "package backend\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/promet"
},
{
"path": "backend/azure_kv_metrics_test.go",
"chars": 1823,
"preview": "package backend\n\nimport (\n\t\"testing\"\n\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/stretchr/t"
},
{
"path": "backend/azure_kv_test.go",
"chars": 8096,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sd"
},
{
"path": "backend/backend.go",
"chars": 2013,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/tuenti/secrets-manager/errors\"\n)\n\n"
},
{
"path": "backend/backend_test.go",
"chars": 2333,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/go-logr/logr\"\n\t\""
},
{
"path": "backend/decoder.go",
"chars": 1741,
"preview": "package backend\n\nimport (\n\t\"encoding/base64\"\n\n\t\"github.com/tuenti/secrets-manager/errors\"\n)\n\nconst (\n\t// Base64EncodingT"
},
{
"path": "backend/decoder_test.go",
"chars": 1659,
"preview": "package backend\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/tuenti/secrets-manager/e"
},
{
"path": "backend/vault.go",
"chars": 7548,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\""
},
{
"path": "backend/vault_engine.go",
"chars": 943,
"preview": "package backend\n\nimport (\n\t\"github.com/hashicorp/vault/api\"\n\t\"github.com/tuenti/secrets-manager/errors\"\n)\n\nconst (\n\tkvEn"
},
{
"path": "backend/vault_engine_test.go",
"chars": 1544,
"preview": "package backend\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/vault/api\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"g"
},
{
"path": "backend/vault_metrics.go",
"chars": 4133,
"preview": "package backend\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/promet"
},
{
"path": "backend/vault_metrics_test.go",
"chars": 4060,
"preview": "package backend\n\nimport (\n\t\"testing\"\n\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/stretchr/t"
},
{
"path": "backend/vault_test.go",
"chars": 16498,
"preview": "package backend\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/has"
},
{
"path": "config/crd/bases/secrets-manager.tuenti.io_secretdefinitions.yaml",
"chars": 2973,
"preview": "\n---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n annotations:\n controller-gen.kube"
},
{
"path": "config/crd/kustomization.yaml",
"chars": 943,
"preview": "# This kustomization.yaml is not intended to be run by itself,\n# since it depends on service name and namespace that are"
},
{
"path": "config/crd/kustomizeconfig.yaml",
"chars": 506,
"preview": "# This file is for teaching kustomize how to substitute name and namespace reference in CRD\nnameReference:\n- kind: Servi"
},
{
"path": "config/crd/patches/cainjection_in_secretdefinitions.yaml",
"chars": 321,
"preview": "# The following patch adds a directive for certmanager to inject CA into the CRD\napiVersion: apiextensions.k8s.io/v1\nkin"
},
{
"path": "config/crd/patches/webhook_in_secretdefinitions.yaml",
"chars": 409,
"preview": "# The following patch enables a conversion webhook for the CRD\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceD"
},
{
"path": "config/default/kustomization.yaml",
"chars": 2580,
"preview": "# Adds namespace to all resources.\nnamespace: secrets-manager-system\n\n# Value of this field is prepended to the\n# names "
},
{
"path": "config/default/manager_auth_proxy_patch.yaml",
"chars": 790,
"preview": "# This patch inject a sidecar container which is a HTTP proxy for the\n# controller manager, it performs RBAC authorizati"
},
{
"path": "config/default/manager_config_patch.yaml",
"chars": 478,
"preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: controller-manager\n namespace: system\nspec:\n template:\n spec"
},
{
"path": "config/default/manager_image_patch.yaml",
"chars": 310,
"preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: controller-manager\n namespace: system\nspec:\n template:\n spec"
},
{
"path": "config/manager/controller_manager_config.yaml",
"chars": 270,
"preview": "apiVersion: controller-runtime.sigs.k8s.io/v1alpha1\nkind: ControllerManagerConfig\nhealth:\n healthProbeBindAddress: :808"
},
{
"path": "config/manager/kustomization.yaml",
"chars": 163,
"preview": "resources:\n- manager.yaml\n\ngeneratorOptions:\n disableNameSuffixHash: true\n\nconfigMapGenerator:\n- name: manager-config\n "
},
{
"path": "config/manager/manager.yaml",
"chars": 1239,
"preview": "apiVersion: v1\nkind: Namespace\nmetadata:\n labels:\n control-plane: controller-manager\n name: system\n---\napiVersion: "
},
{
"path": "config/prometheus/kustomization.yaml",
"chars": 26,
"preview": "resources:\n- monitor.yaml\n"
},
{
"path": "config/prometheus/monitor.yaml",
"chars": 491,
"preview": "\n# Prometheus Monitor Service (Metrics)\napiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n labels:\n "
},
{
"path": "config/rbac/auth_proxy_client_clusterrole.yaml",
"chars": 150,
"preview": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n name: metrics-reader\nrules:\n- nonResourceURLs:\n "
},
{
"path": "config/rbac/auth_proxy_role.yaml",
"chars": 280,
"preview": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n name: proxy-role\nrules:\n- apiGroups:\n - authenti"
},
{
"path": "config/rbac/auth_proxy_role_binding.yaml",
"chars": 268,
"preview": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n name: proxy-rolebinding\nroleRef:\n apiGrou"
},
{
"path": "config/rbac/auth_proxy_service.yaml",
"chars": 268,
"preview": "apiVersion: v1\nkind: Service\nmetadata:\n labels:\n control-plane: controller-manager\n name: controller-manager-metric"
},
{
"path": "config/rbac/kustomization.yaml",
"chars": 693,
"preview": "resources:\n# All RBAC will be applied under this service account in\n# the deployment namespace. You may comment out this"
},
{
"path": "config/rbac/leader_election_role.yaml",
"chars": 476,
"preview": "# permissions to do leader election.\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n name: leader-electi"
},
{
"path": "config/rbac/leader_election_role_binding.yaml",
"chars": 274,
"preview": "apiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n name: leader-election-rolebinding\nroleRef:\n apiG"
},
{
"path": "config/rbac/role.yaml",
"chars": 641,
"preview": "\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n creationTimestamp: null\n name: manager-role"
},
{
"path": "config/rbac/role_binding.yaml",
"chars": 272,
"preview": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n name: manager-rolebinding\nroleRef:\n apiGr"
},
{
"path": "config/rbac/secretdefinition_editor_role.yaml",
"chars": 457,
"preview": "# permissions for end users to edit secretdefinitions.\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetada"
},
{
"path": "config/rbac/secretdefinition_viewer_role.yaml",
"chars": 414,
"preview": "# permissions for end users to view secretdefinitions.\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetada"
},
{
"path": "config/rbac/service_account.yaml",
"chars": 93,
"preview": "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: controller-manager\n namespace: system\n"
},
{
"path": "config/samples/README.md",
"chars": 775,
"preview": "### Deployment sample\n\nThis examples allows you to deploy vault and secrets-manager in your own cluster, using microk8s."
},
{
"path": "config/samples/crd.yaml",
"chars": 2973,
"preview": "\n---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n annotations:\n controller-gen.kube"
},
{
"path": "config/samples/secrets-manager.yaml",
"chars": 3152,
"preview": "---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n labels:\n app: secrets-manager\n name: secrets-manager\n namespace"
},
{
"path": "config/samples/secretsmanager_v1alpha1_secretdefinition.yaml",
"chars": 331,
"preview": "---\napiVersion: secrets-manager.tuenti.io/v1alpha1\nkind: SecretDefinition\nmetadata:\n name: secretdefinition-sample\nspec"
},
{
"path": "config/samples/vault-setup.sh",
"chars": 1117,
"preview": "#!/bin/sh\nexport VAULT_ADDR=http://localhost:8200\necho \"Waiting vault to launch on 8200...\"\n\nwhile ! nc -z localhost 820"
},
{
"path": "config/samples/vault.yaml",
"chars": 917,
"preview": "---\napiVersion: v1\nkind: Service\nmetadata:\n name: vault\n labels:\n app: vault\nspec:\n ports:\n - name: vault\n "
},
{
"path": "controllers/metrics.go",
"chars": 1234,
"preview": "package controllers\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/pr"
},
{
"path": "controllers/secretdefinition_controller.go",
"chars": 11054,
"preview": "/*\nCopyright 2021.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in "
},
{
"path": "controllers/secretdefinition_controller_test.go",
"chars": 14687,
"preview": "package controllers\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"time\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gom"
},
{
"path": "controllers/suite_test.go",
"chars": 4156,
"preview": "/*\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with "
},
{
"path": "deploy/Dockerfile",
"chars": 1397,
"preview": "# Build the manager binary\nFROM golang:1.16 as builder\n\nWORKDIR /workspace\n# Copy the Go Modules manifests\nCOPY go.mod g"
},
{
"path": "deploy/version/get.sh",
"chars": 98,
"preview": "#!/bin/bash\n\nawk '{ split($0,parts,\"=\"); print parts[2]}' $(pwd)/deploy/version/version.properties"
},
{
"path": "deploy/version/update.sh",
"chars": 1150,
"preview": "#!/bin/bash\n\nversion_properties_file=$(pwd)/deploy/version/version.properties\n\nupdate_major() {\n new_version=\"v$(awk "
},
{
"path": "deploy/version/version.properties",
"chars": 15,
"preview": "version=v2.1.0\n"
},
{
"path": "docker-compose.yaml",
"chars": 983,
"preview": "version: '3.4'\nservices:\n secrets-manager-local:\n build:\n context: .\n target: dev\n dockerfile: ./depl"
},
{
"path": "errors/errors.go",
"chars": 4308,
"preview": "package errors\n\nimport \"fmt\"\n\n// Error Types constants\nconst (\n\tUnknownErrorType = \"UnknownError\"\n\tBac"
},
{
"path": "errors/errors_test.go",
"chars": 3683,
"preview": "package errors\n\nimport (\n\te \"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestErrorString(t "
},
{
"path": "go.mod",
"chars": 785,
"preview": "module github.com/tuenti/secrets-manager\n\ngo 1.16\n\nrequire (\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0\n\tgith"
},
{
"path": "go.sum",
"chars": 92565,
"preview": "bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=\ncloud.google.co"
},
{
"path": "hack/boilerplate.go.txt",
"chars": 546,
"preview": "/*\nCopyright 2021.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in "
},
{
"path": "main.go",
"chars": 10032,
"preview": "/*\nCopyright 2021.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in "
},
{
"path": "scripts/setup-dev-env.sh",
"chars": 211,
"preview": "#!/bin/bash\n\n# Download tools\ngo get -v github.com/golang/mock/gomock\ngo get -v github.com/golang/mock/mockgen\ngo get -v"
}
]
About this extraction
This page contains the full source code of the tuenti/secrets-manager GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 77 files (278.1 KB), approximately 101.2k tokens, and a symbol index with 230 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.