Full Code of tuenti/secrets-manager for AI

master 3fd9e760532d cached
77 files
278.1 KB
101.2k tokens
230 symbols
1 requests
Download .txt
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
[![CircleCI](https://circleci.com/gh/tuenti/secrets-manager/tree/master.svg?style=svg)](https://circleci.com/gh/tuenti/secrets-manager/tree/master)
[![Go Report Card](https://goreportcard.com/badge/github.com/tuenti/secrets-manager)](https://goreportcard.com/report/github.com/tuenti/secrets-manager)
[![codecov](https://codecov.io/gh/tuenti/secrets-manager/branch/master/graph/badge.svg)](https://codecov.io/gh/tuenti/secrets-manager)

A tool to keep your Kubernetes secrets in sync with Vault

# Rationale

Lots of companies use [Vault](https://www.vaultproject.io) as their secrets store backend for multiple kind of secrets and different purposes. Kubernetes brings a nice secrets API, but it means that you have two different sources of truth for your secrets.

*secrets-manager* tries to solve this, by reading secrets from Vault and comparing them to Kubernetes secrets, creating and updating them as you do it in Vault.

# How does it compare to other tools?

- [cert-manager](https://github.com/jetstack/cert-manager). *cert-manager* solves a different issue, automation around issuing and renewing certificates. It integrates with Let's Encrypt and Vault (using the pki backend) being those the certificates issuer. While this is really powerful and really a tool which is fully compatible with *secrets-manager*, it does not really sync a secret from a secret backend. *secrets-manager* is a more generic tool where you can sync certificates or any kind of secret from the source of truth of your secrets to Kubernetes secrets.

- [vault-operator](https://github.com/coreos/vault-operator). This manages vault clusters in Kubernetes, so it is a completely different tool.

- [vault-crd](https://github.com/DaspawnW/vault-crd). This is the tool that really inspired *secrets-manager*. We opened this [issue](https://github.com/DaspawnW/vault-crd/issues/4) asking for token renewal or other login mechanism. While the author is very responsive answering, we could not wait for an implementation and since we were are more familiar with Go than Java we decided to write *secrets-manager*. We are very thankful to the author of *vault-crd*, since it has been really inspiring. Some differences:
  - *vault-crd* only supports Hashicorp Vault as its secrets manager, while *secrets-manager* has been designed to support other backends (we only support Vault for now, though).
  - *vault-crd* supports KV1, KV2 and PKI secret engines. *secrets-manager* supports KV1 and KV2. It is on our roadmap to support more secret engines.

# How it works

*secrets-manager* will login to Vault using AppRole credentials and it will start a reconciliation loop watching for changes in `SecretsDefinition` objects. In background it will run two main operations:

- If Vault token is close to expire and if that's the case, renewing it. If it can't renew, it will try to re-login.
- It will re-queue `SecretsDefinition` events and in every event loop it will verify if the current Kubernetes secret it is in the desired state by comparing it with the data in Vault and creating/updating them accordingly

## Custom Resource Definition (CRD)

*secrets-manager* now uses [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) to extend Kubernetes APIs with a new `SecretDefinition` object that it will watch.

To install the CRD in your cluster: `kubectl apply -f crd.yaml`


### Secrets Definition

- `name`: This will be the name of the secret created in Kubernetes.
- `type`: Kubernetes secret type. One of `kubernetes.io/tls`, `Opaque`.
- `keysMap`: This will contain the Kubernetes secret data keys as a map of datasources. Each datasource will contain the way to access the secret in the secret backend source of truth, via a `path` and  a `key`. And optional `encoding` key can be provided if your secrets are codified in `base64`. The absence of `encoding` or `encoding: text` means no encoding.

**NOTE**: We let the user all the responsibility to set the whole Vault path. So it is important to know which path a secret engine needs to be set. For instance, with the KV version 1 all secrets are stored in `secret/` whereas with the KV version 2, all secrets go under `secret/data/`

An example of a `secretdefinition` object

```
$ cat > secretdefinition-sample.yaml <<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
Download .txt
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
Download .txt
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[![CircleCI](https://circleci.com/gh/tuenti/secrets-manager/tree/master.svg?style=svg)](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.

Copied to clipboard!