├── .github ├── PAUL.yaml ├── issue_template.md └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── PROJECT ├── README.md ├── apis ├── secrets │ ├── externalsecret.go │ └── v1alpha1 │ │ ├── externalsecret_types.go │ │ ├── groupversion_info.go │ │ └── zz_generated.deepcopy.go └── store │ ├── secretstore.go │ └── v1alpha1 │ ├── groupversion_info.go │ ├── secretstore_types.go │ └── zz_generated.deepcopy.go ├── assets └── architecture.png ├── codecov.yaml ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── secrets.externalsecret-operator.container-solutions.com_externalsecrets.yaml │ │ └── store.externalsecret-operator.container-solutions.com_secretstores.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_externalsecrets.yaml │ │ ├── cainjection_in_secretstores.yaml │ │ ├── webhook_in_externalsecrets.yaml │ │ └── webhook_in_secretstores.yaml ├── credentials │ ├── credentials-akv.yaml │ ├── credentials-asm.yaml │ ├── credentials-credstash.yaml │ ├── credentials-dummy.yaml │ ├── credentials-gitlab.yaml │ ├── credentials-gsm.yaml │ └── kustomization.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── manager │ ├── 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 │ ├── externalsecret_editor_role.yaml │ ├── externalsecret_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── secretstore_editor_role.yaml │ └── secretstore_viewer_role.yaml ├── samples │ ├── kustomization.yaml │ ├── secrets_v1alpha1_externalsecret.yaml │ └── store_v1alpha1_secretstore.yaml ├── scorecard │ ├── bases │ │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ │ ├── basic.config.yaml │ │ └── olm.config.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── service.yaml ├── controllers ├── secrets │ ├── externalsecret_controller.go │ ├── externalsecret_controller_test.go │ └── suite_test.go └── store │ ├── secretstore_controller.go │ ├── secretstore_controller_test.go │ └── suite_test.go ├── docs ├── backends │ ├── akv.md │ ├── credstash.md │ ├── gitlab.md │ └── gsm.md └── spec │ ├── ExternalSecret.md │ └── SecretStore.md ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── main.go └── pkg ├── akv ├── backend.go └── backend_test.go ├── asm ├── backend.go └── backend_test.go ├── backend ├── backend.go ├── backend_test.go └── doc.go ├── config ├── config.go └── config_test.go ├── credstash ├── backend.go └── backend_test.go ├── dummy ├── backend.go └── backend_test.go ├── gitlab └── backend.go ├── gsm ├── backend.go └── backend_test.go ├── internal └── internal.go ├── register ├── register.go └── register_test.go ├── utils ├── utils.go ├── utils_suite_test.go └── utils_test.go └── version └── version.go /.github/PAUL.yaml: -------------------------------------------------------------------------------- 1 | maintainers: 2 | - knelasevero 3 | - riccardomc 4 | - frankscholten 5 | - iamcaleberic 6 | - mircea-cosbuc 7 | # Allows for the /label and /remove-label commands 8 | # usage: /label enhancement 9 | # usage: /remove-label enhancement 10 | # Will only add existing labels 11 | # Can be used on PR's or Issues 12 | labels: true 13 | # Checks if an issue or an Pull request has a description 14 | empty_description_check: 15 | enabled: true 16 | enforced: true 17 | # Settings for branch destroyer 18 | # branch destroyer will not delete your default branch 19 | # set other "protected" branches here 20 | branch_destroyer: 21 | enabled: true 22 | protected_branches: 23 | - master 24 | - main 25 | pull_requests: 26 | # Specifies whether to allow for automated merging of Pull Requests 27 | automated_merge: true 28 | # Paul will mark a pull request as "stale" if a Pull Request is not updated for this amount of days 29 | # stale_time: 15 30 | # This will limit the amount of PR's a single contributer can have 31 | # Limits work in progress 32 | limit_pull_requests: 33 | max_number: 7 34 | # This is the message that will displayed when a user opens a pull request 35 | open_message: | 36 | Greetings! 37 | Thank you for contributing to this project! 38 | If this is your first time contributing to this project, please make 39 | sure to read the CONTRIBUTING.md 40 | # Enables the /cat command 41 | cats_enabled: true 42 | # enables the /dog command 43 | dogs_enabled: true 44 | # Allows any maintainer in the list to run /approve 45 | # Paul will approve the PR (Does not merge it) 46 | allow_approval: true 47 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | **Describe the solution you'd like** 2 | Describe the end goal of this proposal. What is this new functionality or the new behaviour (or what problem does it fix)? 3 | 4 | **What is the added value?** 5 | Explain the value that it adds. e.g. "Secret refreshing will make internal secrets up to date with external secrets". 6 | 7 | **Give us examples of the outcome** 8 | 9 | Provide templates if you are proposing changes in the CRD. Provide example workflows or code snippets if they make sense to present. 10 | 11 | **Observations (Constraints, Context, etc):** 12 | 13 | Give here all extra information that could be interesting. Such as Golang version and Kubernetes version if you are reporting a bug/problem. You can also foresee technical constrains like "this could only be implementing using this specific technology or approach, because of this and that". 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: [ master ] 10 | 11 | env: 12 | KUBEBUILDER_VERSION: 2.3.1 13 | 14 | jobs: 15 | 16 | build: 17 | name: Build 18 | container: 19 | image: golang:1.15 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v2 25 | 26 | - name: Get dependencies 27 | run: | 28 | go get -v -t -d ./... 29 | 30 | - name: Add kubebuilder 31 | run: | 32 | curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${{env.KUBEBUILDER_VERSION}}/kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz > kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz 33 | tar -xvf kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64.tar.gz 34 | mv kubebuilder_${{env.KUBEBUILDER_VERSION}}_linux_amd64 /usr/local/kubebuilder 35 | 36 | - name: Vet and Build 37 | run: make manager 38 | 39 | - name: Test 40 | run: make test 41 | 42 | - name: Coverage 43 | uses: codecov/codecov-action@v1 44 | with: 45 | # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 46 | file: ./cover.out 47 | # flags: unittests # optional 48 | name: externalsecret-operator 49 | fail_ci_if_error: true 50 | 51 | docker: 52 | name: Docker 53 | runs-on: ubuntu-latest 54 | needs: build 55 | steps: 56 | - name: Prepare 57 | id: prep 58 | run: | 59 | IS_LATEST=false 60 | IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') 61 | DOCKER_IMAGE=ghcr.io/${IMAGE_REPOSITORY} 62 | VERSION=edge 63 | 64 | if [[ $GITHUB_REF == refs/tags/* ]]; then 65 | VERSION=${GITHUB_REF#refs/tags/} 66 | IS_LATEST=true 67 | elif [[ $GITHUB_REF == refs/heads/* ]]; then 68 | VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') 69 | if [[ $GITHUB_REF == refs/heads/master ]]; then 70 | IS_LATEST=true 71 | fi 72 | elif [[ $GITHUB_REF == refs/pull/* ]]; then 73 | VERSION=pr-${{ github.event.number }} 74 | fi 75 | 76 | TAGS="${DOCKER_IMAGE}:${VERSION}" 77 | 78 | if [ "$IS_LATEST" = true ] ; then 79 | TAGS="$TAGS,${DOCKER_IMAGE}:latest" 80 | fi 81 | 82 | if [ "${{ github.event_name }}" = "push" ]; then 83 | TAGS="$TAGS,${DOCKER_IMAGE}:sha-${GITHUB_SHA::8}" 84 | fi 85 | 86 | PUSH_IMAGE=true 87 | REPO_FULL_NAME="${{ github.event.pull_request.head.repo.full_name }}" 88 | # If this is both a pull request and a fork, then don't push the image 89 | if [[ ${{ github.event_name }} == pull_request ]]; then 90 | if [[ $REPO_FULL_NAME != ${{ github.repository }} ]]; then 91 | PUSH_IMAGE=false 92 | fi 93 | fi 94 | 95 | echo ::set-output name=version::${VERSION} 96 | echo ::set-output name=tags::${TAGS} 97 | echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') 98 | echo ::set-output name=push_image::$PUSH_IMAGE 99 | 100 | - name: Check out the repo 101 | uses: actions/checkout@v2 102 | 103 | - name: Set up QEMU 104 | id: qemu 105 | uses: docker/setup-qemu-action@v1 106 | with: 107 | platforms: all 108 | 109 | - name: Set up Docker Buildx 110 | id: buildx 111 | uses: docker/setup-buildx-action@v1 112 | 113 | - name: Login to Github Packages 114 | id: docker-login 115 | uses: docker/login-action@v1 116 | with: 117 | registry: ghcr.io 118 | username: ${{ secrets.GHCR_USERNAME }} 119 | password: ${{ secrets.GHCR_TOKEN }} 120 | if: ${{ steps.prep.outputs.push_image == 'true' }} 121 | 122 | - name: Build and push 123 | id: docker_build 124 | uses: docker/build-push-action@v2 125 | with: 126 | context: . 127 | file: ./Dockerfile 128 | builder: ${{ steps.buildx.outputs.name }} 129 | platforms: linux/amd64,linux/arm/v7,linux/arm64 130 | tags: ${{ steps.prep.outputs.tags }} 131 | push: ${{ steps.prep.outputs.push_image }} 132 | labels: | 133 | org.opencontainers.image.source=${{ github.event.repository.clone_url }} 134 | org.opencontainers.image.created=${{ steps.prep.outputs.created }} 135 | org.opencontainers.image.revision=${{ github.sha }} 136 | 137 | - name: Image digest 138 | run: echo ${{ steps.docker_build.outputs.digest }} 139 | helm: 140 | name: Helm 141 | container: 142 | image: golang:1.15 143 | runs-on: ubuntu-latest 144 | needs: docker 145 | 146 | steps: 147 | - uses: actions/checkout@v2 148 | - name: Test Helm 149 | run: echo "Helm WIP" 150 | 151 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary Build Files 2 | build/_output 3 | build/_test 4 | # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 5 | ### Emacs ### 6 | # -*- mode: gitignore; -*- 7 | *~ 8 | \#*\# 9 | /.emacs.desktop 10 | /.emacs.desktop.lock 11 | *.elc 12 | auto-save-list 13 | tramp 14 | .\#* 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | # flymake-mode 19 | *_flymake.* 20 | # eshell files 21 | /eshell/history 22 | /eshell/lastdir 23 | # elpa packages 24 | /elpa/ 25 | # reftex files 26 | *.rel 27 | # AUCTeX auto folder 28 | /auto/ 29 | # cask packages 30 | .cask/ 31 | dist/ 32 | # Flycheck 33 | flycheck_*.el 34 | # server auth directory 35 | /server/ 36 | # projectiles files 37 | .projectile 38 | projectile-bookmarks.eld 39 | # directory configuration 40 | .dir-locals.el 41 | # saveplace 42 | places 43 | # url cache 44 | url/cache/ 45 | # cedet 46 | ede-projects.el 47 | # smex 48 | smex-items 49 | # company-statistics 50 | company-statistics-cache.el 51 | # anaconda-mode 52 | anaconda-mode/ 53 | ### Go ### 54 | # Binaries for programs and plugins 55 | *.exe 56 | *.exe~ 57 | *.dll 58 | *.so 59 | *.dylib 60 | # Test binary, build with 'go test -c' 61 | *.test 62 | # Output of the go coverage tool, specifically when used with LiteIDE 63 | *.out 64 | ### Vim ### 65 | # swap 66 | .sw[a-p] 67 | .*.sw[a-p] 68 | # session 69 | Session.vim 70 | # temporary 71 | .netrwhist 72 | # auto-generated tag files 73 | tags 74 | ### VisualStudioCode ### 75 | .vscode/* 76 | .history 77 | # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 78 | # Coverage 79 | coverage.txt 80 | operator-sdk 81 | # conf* 82 | # Binaries for programs and plugins 83 | bin 84 | 85 | # Test binary, build with `go test -c` 86 | *.test 87 | 88 | # Output of the go coverage tool, specifically when used with LiteIDE 89 | *.out 90 | 91 | # Kubernetes Generated files - skip generated files, except for vendored files 92 | 93 | !vendor/**/zz_generated.* 94 | 95 | # editor and IDE paraphernalia 96 | .idea 97 | *.swp 98 | *.swo 99 | *~ 100 | conf-dummy.json 101 | conf-op.json 102 | conf.json 103 | encrypt.key 104 | encrypt.key.enc -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | - Updated CRD 3 | - ExternalSecret 4 | - SecretStore 5 | - Support backends additions 6 | - gsm 7 | # Fixes 8 | 9 | 10 | #91: 11 | 12 | - Operator SDK updated to 1.0.1. 13 | - OPERATOR_NAME not used in new controller-runtime, using - - - LeaderElectionID instead 14 | deploy/ folder replaced with config/ which is handled by kustomize 15 | - CRD manifests in deploy/crds/ are now in config/crd/bases 16 | - CR manifests in deploy/crds/ are now in config/samples 17 | - Controller manifest deploy/operator.yaml is now in config/manager/manager.yaml 18 | - RBAC manifests in deploy are now in config/rbac/ 19 | - Go updated to 1.15 20 | - Added tests to the externalsecret-controller 21 | - Helpers and options to work with webhooks 22 | use multigroup: true incase we need to support more complex secrets 23 | - Add config/backend-config to handle backend-config secrets 24 | Previous operator code in /legacy 25 | 26 | - #47 - Add Dockerfile build stage and use https://github.com/GoogleContainerTools/distroless - generated by operator-sdk 27 | - #3 - Introduce support for GCP secret manager(gsm) https://cloud.google.com/secret-manager/docs/configuring-secret-manager 28 | 29 | - #86 - Migrate to github actions from circle ci 30 | 31 | 32 | - #105 33 | - #97 34 | - #7 -Secret Binary Support 35 | - #92 36 | - #24 37 | - #106 38 | - #42 39 | - #29 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Thank you! :tada: 4 | First of all, thank you for exploring the possibility of contributing to this project. 5 | 6 | When contributing to this repository, we would appreciate if you could first discuss the 7 | change you wish to make via issue, email, or any other method with the owners of this 8 | repository before making a change. 9 | 10 | Please note we have a code of conduct, please follow it in all your interactions with the 11 | project. 12 | 13 | ## Local development 14 | 15 | To add new features to this project @riccardomc suggests to develop locally using 16 | [minikube](https://minikube.sigs.k8s.io/docs/start/) or 17 | [k3s](https://k3s.io/) (or [k3d](https://github.com/rancher/k3d)) to check your changes 18 | live. If you have better ideas, feel free to do so and please reach out with suggestions. 19 | 20 | ### Building the code 21 | 22 | The build process is entirely automated and it uses the 23 | [operator-sdk](https://github.com/operator-framework/operator-sdk) executable. 24 | 25 | ``` 26 | make docker-build 27 | ``` 28 | 29 | There's a comprehensive set of unit tests that can be run with: 30 | 31 | ``` 32 | make test 33 | ``` 34 | 35 | ### Use minikube Docker environment 36 | 37 | You can use minikube's Docker daemon to build the externalsecret-operator image. In this 38 | way the image will be automatically available in your minikube instance. 39 | 40 | This flow allows you to deploy your changes with an acceptably short feedback loop: 41 | ``` 42 | minikube start --driver=docker 43 | eval $(minikube -p minikube docker-env) 44 | make docker-build 45 | IMG= make deploy 46 | 47 | * make your changes * 48 | 49 | make docker-build 50 | kubectl get pods -n externalsecret-operator-system | grep externalsecret-operator | awk '{print $1}' | xargs kubectl delete pods 51 | 52 | * make more changes * 53 | 54 | make docker-build 55 | kubectl get pods -n externalsecret-operator-system | grep externalsecret-operator | awk '{print $1}' | xargs kubectl delete pods 56 | 57 | ... 58 | ``` 59 | 60 | ### Image cache 61 | 62 | A similar result can be obtained by building the images using the local Docker daemon and 63 | copying images to minikube cache: 64 | 65 | ``` 66 | make docker-build 67 | minikube cache add containersol/externalsecret-operator 68 | kubectl get pods | grep externalsecret-operator | awk '{print $1}' | xargs kubectl delete pods 69 | ``` 70 | 71 | or k3d: 72 | ``` 73 | make docker-build 74 | k3d import-images --name mycluster containersol/externalsecret-operator 75 | kubectl get pods | grep externalsecret-operator | awk '{print $1}' | xargs kubectl delete pods 76 | ``` 77 | 78 | ## Testing and CI/CD 79 | 80 | Unit tests and end to end tests are run for each commit. Coverage is calculated and uploaded to [codecov](https://codecov.io/) by Github Actions. 81 | 82 | To run tests and view coverage locally 83 | ```shell 84 | make test 85 | ``` 86 | 87 | [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) at `/usr/local/kubebuilder` is required and recommended to run the controller suite tests. 88 | 89 | To use a local cluster for testing update by uncommenting lines with `useExistingCluster := true` in `controllers/secrets/suite_test.go` 90 | ```go 91 | %cat controllers/secrets/suite_test.go 92 | ... 93 | useExistingCluster := true 94 | 95 | By("bootstrapping test environment") 96 | testEnv = &envtest.Environment{ 97 | UseExistingCluster: &useExistingCluster, 98 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 99 | // AttachControlPlaneOutput: true, 100 | } 101 | ``` 102 | 103 | 104 | 113 | 114 | 117 | 118 | ### Docker images 119 | 120 | The CI/CD approach is very simple and could use some improvements, for now: 121 | 122 | * Docker images are built on pull request to master with the ref `pr-` 123 | * Docker images are also built on master and on tags with the ref 124 | 129 | 130 | Every image generated by the CI/CD flow is pushed to [Docker Hub](https://hub.docker.com/repository/docker/containersol/externalsecret-operator) as `containersol/externalsecret-operator:tag`. 131 | 132 | ## Adding a new backend 133 | 134 | Adding a new backend should be relatively straightforward. Use a separate package that 135 | implements the Backend interface. The Backend interface implements only a handful of 136 | functions and is deliberately kept simple: 137 | 138 | ``` 139 | type Backend interface { 140 | Init(map[string]string) error 141 | Get(string, string) (string, error) 142 | } 143 | ``` 144 | 145 | Where `Init` is intended to be used to initialize the Backend using the parameters map 146 | passed as arguments. `Get` is executed to retrieve a secret string based on the strings 147 | passed as arguments. 148 | 149 | Additionally, backends must be imported in `pkg/controller/register.go` in order to be 150 | registered as available backend. 151 | 152 | Check out the [dummy backend](./pkg/dummy/backend.go) for a simple example that should 153 | get you started. 154 | 155 | ## Pull Request Process 156 | 157 | We don't really have strict or automated policies for pull requests. Just try to be nice 158 | :) 159 | 160 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 161 | build. 162 | 2. Add a good title and description of your pull request. 163 | 3. Try to add meaningful commit messages and keep the commit history tidy (no wip commit 164 | please :)). 165 | 4. Reference the issue you are addressing in your pull request. 166 | 167 | ## Code of Conduct 168 | 169 | ### Our Pledge 170 | 171 | In the interest of fostering an open and welcoming environment, we as 172 | contributors and maintainers pledge to making participation in our project and 173 | our community a harassment-free experience for everyone, regardless of age, body 174 | size, disability, ethnicity, gender identity and expression, level of experience, 175 | nationality, personal appearance, race, religion, or sexual identity and 176 | orientation. 177 | 178 | ### Our Standards 179 | 180 | Examples of behavior that contributes to creating a positive environment 181 | include: 182 | 183 | * Using welcoming and inclusive language 184 | * Being respectful of differing viewpoints and experiences 185 | * Gracefully accepting constructive criticism 186 | * Focusing on what is best for the community 187 | * Showing empathy towards other community members 188 | 189 | Examples of unacceptable behavior by participants include: 190 | 191 | * The use of sexualized language or imagery and unwelcome sexual attention or 192 | advances 193 | * Trolling, insulting/derogatory comments, and personal or political attacks 194 | * Public or private harassment 195 | * Publishing others' private information, such as a physical or electronic 196 | address, without explicit permission 197 | * Other conduct which could reasonably be considered inappropriate in a 198 | professional setting 199 | 200 | ### Our Responsibilities 201 | 202 | Project maintainers are responsible for clarifying the standards of acceptable 203 | behavior and are expected to take appropriate and fair corrective action in 204 | response to any instances of unacceptable behavior. 205 | 206 | Project maintainers have the right and responsibility to remove, edit, or 207 | reject comments, commits, code, wiki edits, issues, and other contributions 208 | that are not aligned to this Code of Conduct, or to ban temporarily or 209 | permanently any contributor for other behaviors that they deem inappropriate, 210 | threatening, offensive, or harmful. 211 | 212 | ### Scope 213 | 214 | This Code of Conduct applies both within project spaces and in public spaces 215 | when an individual is representing the project or its community. Examples of 216 | representing a project or community include using an official project e-mail 217 | address, posting via an official social media account, or acting as an appointed 218 | representative at an online or offline event. Representation of a project may be 219 | further defined and clarified by project maintainers. 220 | 221 | ### Enforcement 222 | 223 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 224 | contacting the project maintainer @riccardomc. All complaints 225 | will be reviewed and investigated and will result in a response that is deemed necessary 226 | and appropriate to the circumstances. The project team is obligated to maintain 227 | confidentiality with regard to the reporter of an incident. Further details of specific 228 | enforcement policies may be posted separately. 229 | 230 | Project maintainers who do not follow or enforce the Code of Conduct in good 231 | faith may face temporary or permanent repercussions as determined by other 232 | members of the project's leadership. 233 | 234 | ### Attribution 235 | 236 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 237 | available at [http://contributor-covenant.org/version/1/4][version] 238 | 239 | [homepage]: http://contributor-covenant.org 240 | [version]: http://contributor-covenant.org/version/1/4/ 241 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.15 as builder 3 | 4 | RUN apt update && apt install unzip -y 5 | 6 | # ARG GOARCH=amd64 7 | ENV CGO_ENABLED=0 8 | ENV GOOS=linux 9 | ENV GO111MODULE=on 10 | 11 | ARG TARGETPLATFORM 12 | RUN go env 13 | 14 | WORKDIR /workspace 15 | # Copy the Go Modules manifests 16 | COPY go.mod go.mod 17 | COPY go.sum go.sum 18 | # cache deps before building and copying source so that we don't need to re-download as much 19 | # and so that source changes don't invalidate our downloaded layer 20 | RUN go mod download 21 | 22 | # Copy the go source 23 | COPY main.go main.go 24 | COPY apis/ apis/ 25 | COPY controllers/ controllers/ 26 | COPY pkg/ pkg/ 27 | 28 | # Build 29 | RUN go build -a -o manager main.go 30 | 31 | # Use distroless as minimal base image to package the manager binary 32 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 33 | FROM --platform=${TARGETPLATFORM:-linux/amd64} gcr.io/distroless/base-debian10@sha256:abe4b6cd34fed3ade2e89ed1f2ce75ddab023ea0d583206cfa4f960b74572c67 34 | WORKDIR / 35 | COPY --from=builder /workspace/manager . 36 | 37 | USER nonroot:nonroot 38 | 39 | ENTRYPOINT ["/manager"] 40 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Current Operator version 2 | VERSION ?= 0.1.0 3 | # Default bundle image tag 4 | BUNDLE_IMG ?= controller-bundle:$(VERSION) 5 | # Options for 'bundle-build' 6 | ifneq ($(origin CHANNELS), undefined) 7 | BUNDLE_CHANNELS := --channels=$(CHANNELS) 8 | endif 9 | ifneq ($(origin DEFAULT_CHANNEL), undefined) 10 | BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) 11 | endif 12 | BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) 13 | 14 | # Image URL to use all building/pushing image targets 15 | IMG ?= ghcr.io/containersolutions/externalsecret-operator 16 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 17 | CRD_OPTIONS ?= "crd:trivialVersions=true" 18 | 19 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 20 | ifeq (,$(shell go env GOBIN)) 21 | GOBIN=$(shell go env GOPATH)/bin 22 | else 23 | GOBIN=$(shell go env GOBIN) 24 | endif 25 | 26 | all: manager 27 | 28 | # Run tests 29 | test: generate fmt vet sec manifests 30 | go test ./... -coverprofile cover.out 31 | 32 | # Build manager binary 33 | manager: generate fmt vet sec 34 | go build -o bin/manager main.go 35 | 36 | # Run against the configured Kubernetes cluster in ~/.kube/config 37 | run: generate fmt vet sec manifests 38 | go run ./main.go 39 | 40 | # Install CRDs into a cluster 41 | install: manifests kustomize 42 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 43 | 44 | # Uninstall CRDs from a cluster 45 | uninstall: manifests kustomize 46 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 47 | 48 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 49 | deploy: manifests kustomize 50 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 51 | $(KUSTOMIZE) build config/default | kubectl apply -f - 52 | 53 | # Generate manifests e.g. CRD, RBAC etc. 54 | manifests: controller-gen 55 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 56 | 57 | # Run go fmt against code 58 | fmt: 59 | go fmt ./... 60 | 61 | # Run go vet against code 62 | vet: 63 | go vet ./... 64 | 65 | # Run go gosec against code 66 | sec: gosec 67 | ${GOSEC} --quiet ./... 68 | 69 | # Generate code 70 | generate: controller-gen 71 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 72 | 73 | # Build the docker image 74 | docker-build: test 75 | docker build . -t ${IMG} 76 | 77 | # Push the docker image 78 | docker-push: 79 | docker push ${IMG} 80 | 81 | # find or download controller-gen 82 | # download controller-gen if necessary 83 | controller-gen: 84 | ifeq (, $(shell which controller-gen)) 85 | @{ \ 86 | set -e ;\ 87 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 88 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 89 | go mod init tmp ;\ 90 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\ 91 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 92 | } 93 | CONTROLLER_GEN=$(GOBIN)/controller-gen 94 | else 95 | CONTROLLER_GEN=$(shell which controller-gen) 96 | endif 97 | 98 | # find or download gosec 99 | # download gosec if necessary 100 | gosec: 101 | ifeq (, $(shell which gosec)) 102 | @{ \ 103 | set -e ;\ 104 | GOSEC_TMP_DIR=$$(mktemp -d) ;\ 105 | cd $$GOSEC_TMP_DIR ;\ 106 | go mod init tmp ;\ 107 | go get github.com/securego/gosec/v2/cmd/gosec@v2.4.0 ;\ 108 | rm -rf $$GOSEC_TMP_DIR ;\ 109 | } 110 | GOSEC=$(GOBIN)/gosec 111 | else 112 | GOSEC=$(shell which gosec) 113 | endif 114 | 115 | kustomize: 116 | ifeq (, $(shell which kustomize)) 117 | @{ \ 118 | set -e ;\ 119 | KUSTOMIZE_GEN_TMP_DIR=$$(mktemp -d) ;\ 120 | cd $$KUSTOMIZE_GEN_TMP_DIR ;\ 121 | go mod init tmp ;\ 122 | go get sigs.k8s.io/kustomize/kustomize/v3@v3.5.4 ;\ 123 | rm -rf $$KUSTOMIZE_GEN_TMP_DIR ;\ 124 | } 125 | KUSTOMIZE=$(GOBIN)/kustomize 126 | else 127 | KUSTOMIZE=$(shell which kustomize) 128 | endif 129 | 130 | # Generate bundle manifests and metadata, then validate generated files. 131 | .PHONY: bundle 132 | bundle: manifests 133 | operator-sdk generate kustomize manifests -q 134 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 135 | $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 136 | operator-sdk bundle validate ./bundle 137 | 138 | # Build the bundle image. 139 | .PHONY: bundle-build 140 | bundle-build: 141 | docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 142 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: externalsecret-operator.container-solutions.com 2 | layout: go.kubebuilder.io/v2 3 | multigroup: true 4 | projectName: externalsecret-operator 5 | repo: github.com/containersolutions/externalsecret-operator 6 | resources: 7 | - group: secrets 8 | kind: ExternalSecret 9 | version: v1alpha1 10 | - group: store 11 | kind: SecretStore 12 | version: v1alpha1 13 | version: 3-alpha 14 | plugins: 15 | go.sdk.operatorframework.io/v2-alpha: {} 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # External Secret Operator 2 | ![github actions](https://github.com/ContainerSolutions/externalsecret-operator/workflows/CI/badge.svg) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/ContainerSolutions/externalsecret-operator)](https://goreportcard.com/report/github.com/ContainerSolutions/externalsecret-operator) [![codecov](https://codecov.io/gh/ContainerSolutions/externalsecret-operator/branch/master/graph/badge.svg)](https://codecov.io/gh/ContainerSolutions/externalsecret-operator) 4 | 5 | This operator reads information from a third party service 6 | like [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) or [AWS SSM](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-paramstore.html) and automatically injects the values as [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/). 7 | 8 | ### Disclaimer ⚠️ 9 | 10 | This project will not be maintained anymore, and we are trying to concentrate afforts on this new colaboration: 11 | 12 | [external-secrets/external-secrets](https://github.com/external-secrets/external-secrets) 13 | 14 | Website: https://www.external-secrets.io/ 15 | 16 | # Table of Contents 17 | 18 | * [Features](#features) 19 | * [Quick start](#quick-start) 20 | * [Kustomize](#kustomize) 21 | * [What does it do?](#what-does-it-do) 22 | * [Architecture](#architecture) 23 | * [Running Tests](#running-tests) 24 | * [Spec](#spec) 25 | * [Other Supported Backends](#secrets-backends) 26 | * [Contributing](#contributing) 27 | 28 | 29 | 30 | 31 | ## Features 32 | 33 | - Secrets are refreshed from time to time allowing you to rotate secrets in your providers and still keep everything up to date inside your k8s cluster. 34 | - Change the refresh interval of the secrets to match your needs. You can even make it 10s if you need to debug something (beware of API rate limits). 35 | - For the AWS Backend we support both simple secrets and binfiles. 36 | - You can get speciffic versions of the secrets or just get latest versions of them. 37 | - If you change something in your ExternalSecret CR, the operator will reconcile it (Even if your refresh interval is big). 38 | - AWS Secret Manager, Credstash (AWS KMS), Azure Key Vault, Google Secret Manager and Gitlab backends supported currently! 39 | 40 | 41 | 42 | ## Quick start 43 | 44 | 45 | 46 | 47 | 48 | 67 | 68 | 69 | 70 | ## Using Kustomize 71 | #### Install the operator CRDs 72 | 73 | - Install CRDs 74 | 75 | ``` 76 | make install 77 | ``` 78 | 79 | 80 | 81 | ## What does it do? 82 | 83 | Given a secret defined in AWS Secrets Manager: 84 | 85 | ```shell 86 | % aws secretsmanager create-secret \ 87 | --name=example-externalsecret-key \ 88 | --secret-string='this string is a secret' 89 | ``` 90 | 91 | and updated aws credentials to be used in `config/credentials/kustomization.yaml` with valid AWS credentials: 92 | 93 | ```yaml 94 | %cat config/credentials/kustomization.yaml 95 | resources: 96 | # - credentials-gsm.yaml 97 | - credentials-asm.yaml 98 | # - credentials-dummy.yaml 99 | # - credentials-gitlab.yaml 100 | # - credentials-akv.yaml 101 | ``` 102 | 103 | ```yaml 104 | %cat config/credentials/credentials-asm.yaml 105 | ... 106 | credentials.json: |- 107 | { 108 | "accessKeyID": "AKIA...", 109 | "secretAccessKey": "cmFuZG9tS2VZb25Eb2Nz...", 110 | "sessionToken": "" 111 | } 112 | ``` 113 | 114 | and an `SecretStore` resource definition like this one: 115 | 116 | ```yaml 117 | % cat config/samples/store_v1alpha1_secretstore.yaml 118 | apiVersion: store.externalsecret-operator.container-solutions.com/v1alpha1 119 | kind: SecretStore 120 | metadata: 121 | name: secretstore-sample 122 | spec: 123 | controller: staging 124 | store: 125 | type: asm 126 | auth: 127 | secretRef: 128 | name: externalsecret-operator-credentials-asm 129 | parameters: 130 | region: eu-west-2 131 | ``` 132 | 133 | and an `ExternalSecret` resource definition like this one: 134 | 135 | ```yaml 136 | % cat config/samples/secrets_v1alpha1_externalsecret.yaml 137 | apiVersion: secrets.externalsecret-operator.container-solutions.com/v1alpha1 138 | kind: ExternalSecret 139 | metadata: 140 | name: externalsecret-sample 141 | spec: 142 | storeRef: 143 | name: externalsecret-operator-secretstore-sample 144 | data: 145 | - key: example-externalsecret-key 146 | version: latest 147 | ``` 148 | 149 | The operator fetches the secret from AWS Secrets Manager and injects it as a 150 | secret: 151 | 152 | ```shell 153 | % make deploy 154 | % kubectl get secret externalsecret-operator-externalsecret-sample -n externalsecret-operator-system \ 155 | -o jsonpath='{.data.example-externalsecret-key}' | base64 -d 156 | this string is a secret 157 | ``` 158 | 159 | 160 | ## Architecture 161 | 162 | In [this article](https://docs.google.com/document/d/1hA6eM0TbRYcsDybiHU4kFYIqkEmDFo5GWNzJ2N398cI) you can find more information about the architecture and design choices. 163 | 164 | Here's a high-level diagram of how things are put together. 165 | 166 | ![architecture](./assets/architecture.png) 167 | 168 | 169 | 170 | 171 | ## Running tests 172 | 173 | Requirements: 174 | 175 | - Golang 1.15 or later 176 | - [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) installed at `/usr/local/kubebuilder` 177 | 178 | Then just: 179 | 180 | ```bash 181 | make test 182 | ``` 183 | 184 | 185 | 186 | ## CRDs Spec 187 | 188 | - See the CRD spec 189 | - [ExternalSecret](./docs/spec/ExternalSecret.md) 190 | - [SecretStore](./docs/spec/SecretStore.md) 191 | 192 | 193 | 194 | ## Other Supported Backends 195 | 196 | We would like to support as many backends as possible and it should be rather easy to write new ones. Currently supported backends are: 197 | | Provider | Backend Doc | 198 | |--------------------------------------------------------------------|--------------------------------------------------------------------| 199 | |[AWS Secrets Manager Info](https://aws.amazon.com/secrets-manager/) | [AWS Secrets Manager Backend Docs](#what-does-it-do) | 200 | |[Credstash Info](https://github.com/fugue/credstash/) | [Credstash (AWS KMS) Docs](docs/backends/credstash.md) | 201 | |[GCP Secret Manager Info](https://cloud.google.com/secret-manager) | [GCP Secret Manager Backend Docs](docs/backends/gsm.md) | 202 | |[Gitlab CI/CD Variables Info](https://docs.gitlab.com/ce/ci/variables/) | [Gitlab CI/CD Variables Backend Docs](docs/backends/gitlab.md) | 203 | |[Azure Key Vault Info](https://docs.microsoft.com/en-us/azure/key-vault/) | [Azure Key Vault Backend Docs](docs/backends/akv.md) | 204 | 205 | 206 | 207 | ## Contributing 208 | 209 | Yay! We welcome and encourage contributions to this project! 210 | 211 | See our [contributing document](./CONTRIBUTING.md) and 212 | [Issues](https://github.com/ContainerSolutions/externalsecret-operator/issues) for 213 | planned improvements and additions. 214 | -------------------------------------------------------------------------------- /apis/secrets/externalsecret.go: -------------------------------------------------------------------------------- 1 | package externalsecret 2 | 3 | import ( 4 | "github.com/containersolutions/externalsecret-operator/apis/secrets/v1alpha1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | ) 7 | 8 | func init() { 9 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 10 | AddToSchemes = append(AddToSchemes, v1alpha1.SchemeBuilder.AddToScheme) 11 | } 12 | 13 | // AddToSchemes may be used to add all resources defined in the project to a Scheme 14 | var AddToSchemes runtime.SchemeBuilder 15 | 16 | // AddToScheme adds all Resources to the Scheme 17 | func AddToScheme(s *runtime.Scheme) error { 18 | return AddToSchemes.AddToScheme(s) 19 | } 20 | -------------------------------------------------------------------------------- /apis/secrets/v1alpha1/externalsecret_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | runtime "k8s.io/apimachinery/pkg/runtime" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // ExternalSecretStoreRef is a reference to the external secret SecretStore 28 | type ExternalSecretStoreRef struct { 29 | // +kubebuilder:validation:Required 30 | // +kubebuilder:validation:MinLength=1 31 | // +kubebuilder:validation:Type=string 32 | Name string `json:"name"` 33 | } 34 | 35 | // ExternalSecretTarget ... 36 | type ExternalSecretTarget struct { 37 | // Name of the target Secret Resource 38 | // defaults to .metadata.name of the ExternalSecret. immutable. 39 | // +kubebuilder:validation:Optional 40 | Name string `json:"name,omitempty"` 41 | // +kubebuilder:validation:Optional 42 | CreationPolicy string `json:"creationPolicy,omitempty"` 43 | // +kubebuilder:validation:Optional 44 | Template runtime.RawExtension `json:"template,omitempty"` 45 | } 46 | 47 | // ExternalSecretData contains Key/Name and Version of keys to be retrieved 48 | type ExternalSecretData struct { 49 | // The Key/Name of the secret held in the ExternalBackend 50 | // +kubebuilder:validation:Required 51 | // +kubebuilder:validation:MinLength=1 52 | Key string `json:"key"` 53 | // Version of the secret to be retrieved 54 | Version string `json:"version,omitempty"` 55 | } 56 | 57 | // ExternalSecretSpec defines the desired state of ExternalSecret 58 | type ExternalSecretSpec struct { 59 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 60 | // Important: Run "make" to regenerate code after modifying this file 61 | 62 | // Secrets 63 | // +kubebuilder:validation:Required 64 | // +kubebuilder:validation:MaxItems=20 65 | // +kubebuilder:validation:MinItems=1 66 | Data []ExternalSecretData `json:"data"` 67 | // SecretStore reference 68 | // +kubebuilder:validation:Required 69 | StoreRef ExternalSecretStoreRef `json:"storeRef"` 70 | 71 | // +kubebuilder:validation:Optional 72 | // Secret Rotation Period; 73 | // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". 74 | RefreshInterval string `json:"refreshInterval,omitempty"` 75 | // +kubebuilder:validation:Optional 76 | Target ExternalSecretTarget `json:"target,omitempty"` 77 | } 78 | 79 | // ExternalSecretStatus defines the observed state of ExternalSecret 80 | type ExternalSecretStatus struct { 81 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 82 | // Important: Run "make" to regenerate code after modifying this file 83 | // Defines where the ExternalSecret is in its lifecycle 84 | Phase string `json:"phase,omitempty"` 85 | // Conditions represent the latest available observations of an object's state 86 | Conditions []metav1.Condition `json:"conditions"` 87 | } 88 | 89 | // +kubebuilder:object:root=true 90 | // +kubebuilder:subresource:status 91 | 92 | // ExternalSecret is the Schema for the externalsecrets API 93 | type ExternalSecret struct { 94 | metav1.TypeMeta `json:",inline"` 95 | metav1.ObjectMeta `json:"metadata,omitempty"` 96 | 97 | Spec ExternalSecretSpec `json:"spec"` 98 | Status ExternalSecretStatus `json:"status,omitempty"` 99 | } 100 | 101 | // +kubebuilder:object:root=true 102 | 103 | // ExternalSecretList contains a list of ExternalSecret 104 | type ExternalSecretList struct { 105 | metav1.TypeMeta `json:",inline"` 106 | metav1.ListMeta `json:"metadata,omitempty"` 107 | Items []ExternalSecret `json:"items"` 108 | } 109 | 110 | func init() { 111 | SchemeBuilder.Register(&ExternalSecret{}, &ExternalSecretList{}) 112 | } 113 | -------------------------------------------------------------------------------- /apis/secrets/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the secrets v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=secrets.externalsecret-operator.container-solutions.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "secrets.externalsecret-operator.container-solutions.com", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /apis/secrets/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *ExternalSecret) DeepCopyInto(out *ExternalSecret) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecret. 38 | func (in *ExternalSecret) DeepCopy() *ExternalSecret { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(ExternalSecret) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *ExternalSecret) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *ExternalSecretData) DeepCopyInto(out *ExternalSecretData) { 57 | *out = *in 58 | } 59 | 60 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretData. 61 | func (in *ExternalSecretData) DeepCopy() *ExternalSecretData { 62 | if in == nil { 63 | return nil 64 | } 65 | out := new(ExternalSecretData) 66 | in.DeepCopyInto(out) 67 | return out 68 | } 69 | 70 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 71 | func (in *ExternalSecretList) DeepCopyInto(out *ExternalSecretList) { 72 | *out = *in 73 | out.TypeMeta = in.TypeMeta 74 | in.ListMeta.DeepCopyInto(&out.ListMeta) 75 | if in.Items != nil { 76 | in, out := &in.Items, &out.Items 77 | *out = make([]ExternalSecret, len(*in)) 78 | for i := range *in { 79 | (*in)[i].DeepCopyInto(&(*out)[i]) 80 | } 81 | } 82 | } 83 | 84 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretList. 85 | func (in *ExternalSecretList) DeepCopy() *ExternalSecretList { 86 | if in == nil { 87 | return nil 88 | } 89 | out := new(ExternalSecretList) 90 | in.DeepCopyInto(out) 91 | return out 92 | } 93 | 94 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 95 | func (in *ExternalSecretList) DeepCopyObject() runtime.Object { 96 | if c := in.DeepCopy(); c != nil { 97 | return c 98 | } 99 | return nil 100 | } 101 | 102 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 103 | func (in *ExternalSecretSpec) DeepCopyInto(out *ExternalSecretSpec) { 104 | *out = *in 105 | if in.Data != nil { 106 | in, out := &in.Data, &out.Data 107 | *out = make([]ExternalSecretData, len(*in)) 108 | copy(*out, *in) 109 | } 110 | out.StoreRef = in.StoreRef 111 | in.Target.DeepCopyInto(&out.Target) 112 | } 113 | 114 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretSpec. 115 | func (in *ExternalSecretSpec) DeepCopy() *ExternalSecretSpec { 116 | if in == nil { 117 | return nil 118 | } 119 | out := new(ExternalSecretSpec) 120 | in.DeepCopyInto(out) 121 | return out 122 | } 123 | 124 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 125 | func (in *ExternalSecretStatus) DeepCopyInto(out *ExternalSecretStatus) { 126 | *out = *in 127 | if in.Conditions != nil { 128 | in, out := &in.Conditions, &out.Conditions 129 | *out = make([]v1.Condition, len(*in)) 130 | for i := range *in { 131 | (*in)[i].DeepCopyInto(&(*out)[i]) 132 | } 133 | } 134 | } 135 | 136 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretStatus. 137 | func (in *ExternalSecretStatus) DeepCopy() *ExternalSecretStatus { 138 | if in == nil { 139 | return nil 140 | } 141 | out := new(ExternalSecretStatus) 142 | in.DeepCopyInto(out) 143 | return out 144 | } 145 | 146 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 147 | func (in *ExternalSecretStoreRef) DeepCopyInto(out *ExternalSecretStoreRef) { 148 | *out = *in 149 | } 150 | 151 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretStoreRef. 152 | func (in *ExternalSecretStoreRef) DeepCopy() *ExternalSecretStoreRef { 153 | if in == nil { 154 | return nil 155 | } 156 | out := new(ExternalSecretStoreRef) 157 | in.DeepCopyInto(out) 158 | return out 159 | } 160 | 161 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 162 | func (in *ExternalSecretTarget) DeepCopyInto(out *ExternalSecretTarget) { 163 | *out = *in 164 | in.Template.DeepCopyInto(&out.Template) 165 | } 166 | 167 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretTarget. 168 | func (in *ExternalSecretTarget) DeepCopy() *ExternalSecretTarget { 169 | if in == nil { 170 | return nil 171 | } 172 | out := new(ExternalSecretTarget) 173 | in.DeepCopyInto(out) 174 | return out 175 | } 176 | -------------------------------------------------------------------------------- /apis/store/secretstore.go: -------------------------------------------------------------------------------- 1 | package secretstore 2 | 3 | import ( 4 | "github.com/containersolutions/externalsecret-operator/apis/store/v1alpha1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | ) 7 | 8 | func init() { 9 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 10 | AddToSchemes = append(AddToSchemes, v1alpha1.SchemeBuilder.AddToScheme) 11 | } 12 | 13 | // AddToSchemes may be used to add all resources defined in the project to a Scheme 14 | var AddToSchemes runtime.SchemeBuilder 15 | 16 | // AddToScheme adds all Resources to the Scheme 17 | func AddToScheme(s *runtime.Scheme) error { 18 | return AddToSchemes.AddToScheme(s) 19 | } 20 | -------------------------------------------------------------------------------- /apis/store/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the store v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=store.externalsecret-operator.container-solutions.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "store.externalsecret-operator.container-solutions.com", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /apis/store/v1alpha1/secretstore_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | runtime "k8s.io/apimachinery/pkg/runtime" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // SecretStoreSpec defines the desired state of SecretStore 28 | type SecretStoreSpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | // +kubebuilder:validation:Required 32 | // +kubebuilder:validation:MinLength=1 33 | // +kubebuilder:validation:Type=string 34 | Controller string `json:"controller"` 35 | Store runtime.RawExtension `json:"store"` 36 | } 37 | 38 | // SecretStoreStatus defines the observed state of SecretStore 39 | type SecretStoreStatus struct { 40 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 41 | // Important: Run "make" to regenerate code after modifying this file 42 | // Defines where the SecretStore is in its lifecycle 43 | Phase string `json:"phase,omitempty"` 44 | // Conditions represent the latest available observations of an object's state 45 | Conditions []metav1.Condition `json:"conditions"` 46 | } 47 | 48 | // +kubebuilder:object:root=true 49 | // +kubebuilder:subresource:status 50 | 51 | // SecretStore is the Schema for the secretstores API 52 | type SecretStore struct { 53 | metav1.TypeMeta `json:",inline"` 54 | metav1.ObjectMeta `json:"metadata,omitempty"` 55 | 56 | Spec SecretStoreSpec `json:"spec"` 57 | Status SecretStoreStatus `json:"status,omitempty"` 58 | } 59 | 60 | // +kubebuilder:object:root=true 61 | 62 | // SecretStoreList contains a list of SecretStore 63 | type SecretStoreList struct { 64 | metav1.TypeMeta `json:",inline"` 65 | metav1.ListMeta `json:"metadata,omitempty"` 66 | Items []SecretStore `json:"items"` 67 | } 68 | 69 | func init() { 70 | SchemeBuilder.Register(&SecretStore{}, &SecretStoreList{}) 71 | } 72 | -------------------------------------------------------------------------------- /apis/store/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *SecretStore) DeepCopyInto(out *SecretStore) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStore. 38 | func (in *SecretStore) DeepCopy() *SecretStore { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(SecretStore) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *SecretStore) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *SecretStoreList) DeepCopyInto(out *SecretStoreList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]SecretStore, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | } 68 | 69 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreList. 70 | func (in *SecretStoreList) DeepCopy() *SecretStoreList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(SecretStoreList) 75 | in.DeepCopyInto(out) 76 | return out 77 | } 78 | 79 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 80 | func (in *SecretStoreList) DeepCopyObject() runtime.Object { 81 | if c := in.DeepCopy(); c != nil { 82 | return c 83 | } 84 | return nil 85 | } 86 | 87 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 88 | func (in *SecretStoreSpec) DeepCopyInto(out *SecretStoreSpec) { 89 | *out = *in 90 | in.Store.DeepCopyInto(&out.Store) 91 | } 92 | 93 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreSpec. 94 | func (in *SecretStoreSpec) DeepCopy() *SecretStoreSpec { 95 | if in == nil { 96 | return nil 97 | } 98 | out := new(SecretStoreSpec) 99 | in.DeepCopyInto(out) 100 | return out 101 | } 102 | 103 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 104 | func (in *SecretStoreStatus) DeepCopyInto(out *SecretStoreStatus) { 105 | *out = *in 106 | if in.Conditions != nil { 107 | in, out := &in.Conditions, &out.Conditions 108 | *out = make([]v1.Condition, len(*in)) 109 | for i := range *in { 110 | (*in)[i].DeepCopyInto(&(*out)[i]) 111 | } 112 | } 113 | } 114 | 115 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreStatus. 116 | func (in *SecretStoreStatus) DeepCopy() *SecretStoreStatus { 117 | if in == nil { 118 | return nil 119 | } 120 | out := new(SecretStoreStatus) 121 | in.DeepCopyInto(out) 122 | return out 123 | } 124 | -------------------------------------------------------------------------------- /assets/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContainerSolutions/externalsecret-operator/2bc4284eadaf8768208aca035dcced1c0715b1f0/assets/architecture.png -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: 18 | layout: "reach,diff,flags,files,footer" 19 | behavior: default 20 | require_changes: no -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for 4 | # breaking changes 5 | apiVersion: cert-manager.io/v1alpha2 6 | kind: Issuer 7 | metadata: 8 | name: selfsigned-issuer 9 | namespace: system 10 | spec: 11 | selfSigned: {} 12 | --- 13 | apiVersion: cert-manager.io/v1alpha2 14 | kind: Certificate 15 | metadata: 16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 17 | namespace: system 18 | spec: 19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 20 | dnsNames: 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: selfsigned-issuer 26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 27 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/bases/secrets.externalsecret-operator.container-solutions.com_externalsecrets.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.3.0 8 | creationTimestamp: null 9 | name: externalsecrets.secrets.externalsecret-operator.container-solutions.com 10 | spec: 11 | group: secrets.externalsecret-operator.container-solutions.com 12 | names: 13 | kind: ExternalSecret 14 | listKind: ExternalSecretList 15 | plural: externalsecrets 16 | singular: externalsecret 17 | scope: Namespaced 18 | subresources: 19 | status: {} 20 | validation: 21 | openAPIV3Schema: 22 | description: ExternalSecret is the Schema for the externalsecrets API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: ExternalSecretSpec defines the desired state of ExternalSecret 38 | properties: 39 | data: 40 | description: Secrets 41 | items: 42 | description: ExternalSecretData contains Key/Name and Version of keys 43 | to be retrieved 44 | properties: 45 | key: 46 | description: The Key/Name of the secret held in the ExternalBackend 47 | minLength: 1 48 | type: string 49 | version: 50 | description: Version of the secret to be retrieved 51 | type: string 52 | required: 53 | - key 54 | type: object 55 | maxItems: 20 56 | minItems: 1 57 | type: array 58 | refreshInterval: 59 | description: Secret Rotation Period; Valid time units are "ns", "us" 60 | (or "µs"), "ms", "s", "m", "h". 61 | type: string 62 | storeRef: 63 | description: SecretStore reference 64 | properties: 65 | name: 66 | minLength: 1 67 | type: string 68 | required: 69 | - name 70 | type: object 71 | target: 72 | description: ExternalSecretTarget ... 73 | properties: 74 | creationPolicy: 75 | type: string 76 | name: 77 | description: ' Name of the target Secret Resource defaults to .metadata.name 78 | of the ExternalSecret. immutable.' 79 | type: string 80 | template: 81 | type: object 82 | type: object 83 | required: 84 | - data 85 | - storeRef 86 | type: object 87 | status: 88 | description: ExternalSecretStatus defines the observed state of ExternalSecret 89 | properties: 90 | conditions: 91 | description: Conditions represent the latest available observations 92 | of an object's state 93 | items: 94 | description: "Condition contains details for one aspect of the current 95 | state of this API Resource. --- This struct is intended for direct 96 | use as an array at the field path .status.conditions. For example, 97 | type FooStatus struct{ // Represents the observations of a foo's 98 | current state. // Known .status.conditions.type are: \"Available\", 99 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // 100 | +patchStrategy=merge // +listType=map // +listMapKey=type 101 | \ Conditions []metav1.Condition `json:\"conditions,omitempty\" 102 | patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` 103 | \n // other fields }" 104 | properties: 105 | lastTransitionTime: 106 | description: lastTransitionTime is the last time the condition 107 | transitioned from one status to another. This should be when 108 | the underlying condition changed. If that is not known, then 109 | using the time when the API field changed is acceptable. 110 | format: date-time 111 | type: string 112 | message: 113 | description: message is a human readable message indicating details 114 | about the transition. This may be an empty string. 115 | maxLength: 32768 116 | type: string 117 | observedGeneration: 118 | description: observedGeneration represents the .metadata.generation 119 | that the condition was set based upon. For instance, if .metadata.generation 120 | is currently 12, but the .status.conditions[x].observedGeneration 121 | is 9, the condition is out of date with respect to the current 122 | state of the instance. 123 | format: int64 124 | minimum: 0 125 | type: integer 126 | reason: 127 | description: reason contains a programmatic identifier indicating 128 | the reason for the condition's last transition. Producers of 129 | specific condition types may define expected values and meanings 130 | for this field, and whether the values are considered a guaranteed 131 | API. The value should be a CamelCase string. This field may 132 | not be empty. 133 | maxLength: 1024 134 | minLength: 1 135 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 136 | type: string 137 | status: 138 | description: status of the condition, one of True, False, Unknown. 139 | enum: 140 | - "True" 141 | - "False" 142 | - Unknown 143 | type: string 144 | type: 145 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 146 | --- Many .condition.type values are consistent across resources 147 | like Available, but because arbitrary conditions can be useful 148 | (see .node.status.conditions), the ability to deconflict is 149 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 150 | maxLength: 316 151 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 152 | type: string 153 | required: 154 | - lastTransitionTime 155 | - message 156 | - reason 157 | - status 158 | - type 159 | type: object 160 | type: array 161 | phase: 162 | description: 'INSERT ADDITIONAL STATUS FIELD - define observed state 163 | of cluster Important: Run "make" to regenerate code after modifying 164 | this file Defines where the ExternalSecret is in its lifecycle' 165 | type: string 166 | required: 167 | - conditions 168 | type: object 169 | required: 170 | - spec 171 | type: object 172 | version: v1alpha1 173 | versions: 174 | - name: v1alpha1 175 | served: true 176 | storage: true 177 | status: 178 | acceptedNames: 179 | kind: "" 180 | plural: "" 181 | conditions: [] 182 | storedVersions: [] 183 | -------------------------------------------------------------------------------- /config/crd/bases/store.externalsecret-operator.container-solutions.com_secretstores.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.3.0 8 | creationTimestamp: null 9 | name: secretstores.store.externalsecret-operator.container-solutions.com 10 | spec: 11 | group: store.externalsecret-operator.container-solutions.com 12 | names: 13 | kind: SecretStore 14 | listKind: SecretStoreList 15 | plural: secretstores 16 | singular: secretstore 17 | scope: Namespaced 18 | subresources: 19 | status: {} 20 | validation: 21 | openAPIV3Schema: 22 | description: SecretStore is the Schema for the secretstores API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: SecretStoreSpec defines the desired state of SecretStore 38 | properties: 39 | controller: 40 | description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 41 | Important: Run "make" to regenerate code after modifying this file' 42 | minLength: 1 43 | type: string 44 | store: 45 | type: object 46 | required: 47 | - controller 48 | - store 49 | type: object 50 | status: 51 | description: SecretStoreStatus defines the observed state of SecretStore 52 | properties: 53 | conditions: 54 | description: Conditions represent the latest available observations 55 | of an object's state 56 | items: 57 | description: "Condition contains details for one aspect of the current 58 | state of this API Resource. --- This struct is intended for direct 59 | use as an array at the field path .status.conditions. For example, 60 | type FooStatus struct{ // Represents the observations of a foo's 61 | current state. // Known .status.conditions.type are: \"Available\", 62 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // 63 | +patchStrategy=merge // +listType=map // +listMapKey=type 64 | \ Conditions []metav1.Condition `json:\"conditions,omitempty\" 65 | patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` 66 | \n // other fields }" 67 | properties: 68 | lastTransitionTime: 69 | description: lastTransitionTime is the last time the condition 70 | transitioned from one status to another. This should be when 71 | the underlying condition changed. If that is not known, then 72 | using the time when the API field changed is acceptable. 73 | format: date-time 74 | type: string 75 | message: 76 | description: message is a human readable message indicating details 77 | about the transition. This may be an empty string. 78 | maxLength: 32768 79 | type: string 80 | observedGeneration: 81 | description: observedGeneration represents the .metadata.generation 82 | that the condition was set based upon. For instance, if .metadata.generation 83 | is currently 12, but the .status.conditions[x].observedGeneration 84 | is 9, the condition is out of date with respect to the current 85 | state of the instance. 86 | format: int64 87 | minimum: 0 88 | type: integer 89 | reason: 90 | description: reason contains a programmatic identifier indicating 91 | the reason for the condition's last transition. Producers of 92 | specific condition types may define expected values and meanings 93 | for this field, and whether the values are considered a guaranteed 94 | API. The value should be a CamelCase string. This field may 95 | not be empty. 96 | maxLength: 1024 97 | minLength: 1 98 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 99 | type: string 100 | status: 101 | description: status of the condition, one of True, False, Unknown. 102 | enum: 103 | - "True" 104 | - "False" 105 | - Unknown 106 | type: string 107 | type: 108 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 109 | --- Many .condition.type values are consistent across resources 110 | like Available, but because arbitrary conditions can be useful 111 | (see .node.status.conditions), the ability to deconflict is 112 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 113 | maxLength: 316 114 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 115 | type: string 116 | required: 117 | - lastTransitionTime 118 | - message 119 | - reason 120 | - status 121 | - type 122 | type: object 123 | type: array 124 | phase: 125 | description: 'INSERT ADDITIONAL STATUS FIELD - define observed state 126 | of cluster Important: Run "make" to regenerate code after modifying 127 | this file Defines where the SecretStore is in its lifecycle' 128 | type: string 129 | required: 130 | - conditions 131 | type: object 132 | required: 133 | - spec 134 | type: object 135 | version: v1alpha1 136 | versions: 137 | - name: v1alpha1 138 | served: true 139 | storage: true 140 | status: 141 | acceptedNames: 142 | kind: "" 143 | plural: "" 144 | conditions: [] 145 | storedVersions: [] 146 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/secrets.externalsecret-operator.container-solutions.com_externalsecrets.yaml 6 | - bases/store.externalsecret-operator.container-solutions.com_secretstores.yaml 7 | # +kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patchesStrategicMerge: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | #- patches/webhook_in_externalsecrets.yaml 13 | #- patches/webhook_in_secretstores.yaml 14 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | #- patches/cainjection_in_externalsecrets.yaml 19 | #- patches/cainjection_in_secretstores.yaml 20 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 21 | 22 | # the following config is for teaching kustomize how to do kustomization for CRDs. 23 | configurations: 24 | - kustomizeconfig.yaml 25 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_externalsecrets.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: externalsecrets.secrets.externalsecret-operator.container-solutions.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_secretstores.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: secretstores.store.externalsecret-operator.container-solutions.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_externalsecrets.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: externalsecrets.secrets.externalsecret-operator.container-solutions.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_secretstores.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: secretstores.store.externalsecret-operator.container-solutions.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/credentials/credentials-akv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: credentials-akv 5 | labels: 6 | type: akv 7 | type: Opaque 8 | stringData: 9 | credentials.json: |- 10 | { 11 | "clientId": "", 12 | "clientSecret": "", 13 | "tenantId": "", 14 | "akvName": "" 15 | } 16 | -------------------------------------------------------------------------------- /config/credentials/credentials-asm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: credentials-asm 5 | labels: 6 | type: asm 7 | type: Opaque 8 | stringData: 9 | credentials.json: |- 10 | { 11 | "accessKeyID": "", 12 | "secretAccessKey": "", 13 | "sessionToken": "" 14 | } 15 | -------------------------------------------------------------------------------- /config/credentials/credentials-credstash.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: credentials-credstash 5 | labels: 6 | type: credstash 7 | type: Opaque 8 | stringData: 9 | credentials.json: |- 10 | { 11 | "accessKeyID": "", 12 | "secretAccessKey": "", 13 | "sessionToken": "" 14 | } -------------------------------------------------------------------------------- /config/credentials/credentials-dummy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: credentials-dummy 5 | labels: 6 | type: dummy 7 | type: Opaque 8 | stringData: 9 | credentials.json: |- 10 | { 11 | "Credential": "-dummyvalue" 12 | } 13 | -------------------------------------------------------------------------------- /config/credentials/credentials-gitlab.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: credentials-gitlab 5 | labels: 6 | type: gitlab 7 | type: Opaque 8 | stringData: 9 | credentials.json: |- 10 | { 11 | "token": "${OP_GITLAB_TOKEN}" 12 | } 13 | 14 | -------------------------------------------------------------------------------- /config/credentials/credentials-gsm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: credentials-gsm 5 | labels: 6 | type: gsm 7 | type: Opaque 8 | stringData: 9 | credentials.json: |- 10 | { 11 | "type": "service_account", 12 | "project_id": "external-secrets-operator", 13 | "private_key_id": "", 14 | "private_key": "-----BEGIN PRIVATE KEY-----\nA key\n-----END PRIVATE KEY-----\n", 15 | "client_email": "test-service-account@external-secrets-operator.iam.gserviceaccount.com", 16 | "client_id": "client ID", 17 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 18 | "token_uri": "https://oauth2.googleapis.com/token", 19 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 20 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40external-secrets-operator.iam.gserviceaccount.com" 21 | } 22 | 23 | -------------------------------------------------------------------------------- /config/credentials/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # - credentials-gsm.yaml 3 | # - credentials-asm.yaml 4 | - credentials-dummy.yaml 5 | # - credentials-gitlab.yaml 6 | # - credentials-akv.yaml 7 | # - credentials-credstash.yaml 8 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: externalsecret-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: externalsecret-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | - ../credentials 20 | - ../samples 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | 29 | patchesStrategicMerge: 30 | # Protect the /metrics endpoint by putting it behind auth. 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, please comment the following line. 33 | - manager_auth_proxy_patch.yaml 34 | 35 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 36 | # crd/kustomization.yaml 37 | #- manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | #- webhookcainjection_patch.yaml 43 | 44 | # the following config is for teaching kustomize how to do var substitution 45 | vars: 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 48 | # objref: 49 | # kind: Certificate 50 | # group: cert-manager.io 51 | # version: v1alpha2 52 | # name: serving-cert # this name should match the one in certificate.yaml 53 | # fieldref: 54 | # fieldpath: metadata.namespace 55 | #- name: CERTIFICATE_NAME 56 | # objref: 57 | # kind: Certificate 58 | # group: cert-manager.io 59 | # version: v1alpha2 60 | # name: serving-cert # this name should match the one in certificate.yaml 61 | #- name: SERVICE_NAMESPACE # namespace of the service 62 | # objref: 63 | # kind: Service 64 | # version: v1 65 | # name: webhook-service 66 | # fieldref: 67 | # fieldpath: metadata.namespace 68 | #- name: SERVICE_NAME 69 | # objref: 70 | # kind: Service 71 | # version: v1 72 | # name: webhook-service 73 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: containersol/externalsecret-operator 8 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | containers: 26 | - command: 27 | - /manager 28 | args: 29 | - --enable-leader-election 30 | image: controller:latest 31 | name: manager 32 | resources: 33 | limits: 34 | cpu: 100m 35 | memory: 30Mi 36 | requests: 37 | cpu: 100m 38 | memory: 20Mi 39 | terminationGracePeriodSeconds: 10 40 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/externalsecret_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit externalsecrets. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: externalsecret-editor-role 6 | rules: 7 | - apiGroups: 8 | - secrets.externalsecret-operator.container-solutions.com 9 | resources: 10 | - externalsecrets 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - secrets.externalsecret-operator.container-solutions.com 21 | resources: 22 | - externalsecrets/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/externalsecret_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view externalsecrets. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: externalsecret-viewer-role 6 | rules: 7 | - apiGroups: 8 | - secrets.externalsecret-operator.container-solutions.com 9 | resources: 10 | - externalsecrets 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - secrets.externalsecret-operator.container-solutions.com 17 | resources: 18 | - externalsecrets/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | - patch 34 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: manager-role 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - secrets 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - secrets.externalsecret-operator.container-solutions.com 23 | resources: 24 | - externalsecrets 25 | verbs: 26 | - create 27 | - delete 28 | - get 29 | - list 30 | - patch 31 | - update 32 | - watch 33 | - apiGroups: 34 | - secrets.externalsecret-operator.container-solutions.com 35 | resources: 36 | - externalsecrets/status 37 | verbs: 38 | - get 39 | - patch 40 | - update 41 | - apiGroups: 42 | - store.externalsecret-operator.container-solutions.com 43 | resources: 44 | - secretstores 45 | verbs: 46 | - create 47 | - delete 48 | - get 49 | - list 50 | - patch 51 | - update 52 | - watch 53 | - apiGroups: 54 | - store.externalsecret-operator.container-solutions.com 55 | resources: 56 | - secretstores/status 57 | verbs: 58 | - get 59 | - patch 60 | - update 61 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/secretstore_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit secretstores. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: secretstore-editor-role 6 | rules: 7 | - apiGroups: 8 | - store.externalsecret-operator.container-solutions.com 9 | resources: 10 | - secretstores 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - store.externalsecret-operator.container-solutions.com 21 | resources: 22 | - secretstores/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/secretstore_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view secretstores. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: secretstore-viewer-role 6 | rules: 7 | - apiGroups: 8 | - store.externalsecret-operator.container-solutions.com 9 | resources: 10 | - secretstores 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - store.externalsecret-operator.container-solutions.com 17 | resources: 18 | - secretstores/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - secrets_v1alpha1_externalsecret.yaml 4 | - store_v1alpha1_secretstore.yaml 5 | # +kubebuilder:scaffold:manifestskustomizesamples 6 | -------------------------------------------------------------------------------- /config/samples/secrets_v1alpha1_externalsecret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: secrets.externalsecret-operator.container-solutions.com/v1alpha1 2 | kind: ExternalSecret 3 | metadata: 4 | name: externalsecret-sample 5 | spec: 6 | storeRef: 7 | name: externalsecret-operator-secretstore-sample 8 | data: 9 | - key: backend-secret-text 10 | version: latest 11 | - key: backend-secret-file 12 | version: latest -------------------------------------------------------------------------------- /config/samples/store_v1alpha1_secretstore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: store.externalsecret-operator.container-solutions.com/v1alpha1 2 | kind: SecretStore 3 | metadata: 4 | name: secretstore-sample 5 | spec: 6 | controller: staging 7 | # Sample store types 8 | # 9 | # Dummy 10 | store: 11 | type: dummy 12 | auth: 13 | secretRef: 14 | name: externalsecret-operator-credentials-dummy 15 | parameters: 16 | Suffix: TestParam 17 | Test: TestParam 18 | 19 | # AWS Secrets Manager 20 | # store: 21 | # type: asm 22 | # auth: 23 | # secretRef: 24 | # name: externalsecret-operator-credentials-asm 25 | # parameters: 26 | # region: eu-west-2 27 | 28 | # GCP Secret Manager 29 | # store: 30 | # type: gsm 31 | # auth: 32 | # secretRef: 33 | # name: externalsecret-operator-credentials-gsm 34 | # parameters: 35 | # projectID: external-secrets-operator 36 | 37 | # Gitlab Project Variables 38 | # store: 39 | # type: gitlab 40 | # auth: 41 | # secretRef: 42 | # name: externalsecret-operator-credentials-gitlab 43 | # parameters: 44 | # baseURL: https://gitlab.com 45 | # projectID: 12345678 46 | 47 | #Azure Key Vault 48 | # store: 49 | # type: akv 50 | # auth: 51 | # secretRef: 52 | # name: externalsecret-operator-credentials-akv 53 | # parameters: {} 54 | 55 | # Credstash Project Variables 56 | # store: 57 | # type: credstash 58 | # auth: 59 | # secretRef: 60 | # name: externalsecret-operator-credentials-credstash 61 | # parameters: 62 | # region: eu-west-2 63 | # table : credential-store 64 | # encryptionContext: 65 | # securityKey: securityValue 66 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | # +kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.0.1 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.0.1 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.0.1 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.0.1 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.0.1 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.0.1 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /controllers/secrets/externalsecret_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "time" 23 | 24 | "github.com/go-logr/logr" 25 | "github.com/prometheus/common/log" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/api/errors" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | "k8s.io/apimachinery/pkg/types" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | 34 | secretsv1alpha1 "github.com/containersolutions/externalsecret-operator/apis/secrets/v1alpha1" 35 | storev1alpha1 "github.com/containersolutions/externalsecret-operator/apis/store/v1alpha1" 36 | 37 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 38 | ) 39 | 40 | const ( 41 | defaulRetryPeriod = time.Second * 30 42 | defaultRefreshInterval = time.Hour * 1 43 | ) 44 | 45 | // ExternalSecretReconciler reconciles a ExternalSecret object 46 | type ExternalSecretReconciler struct { 47 | client.Client 48 | Log logr.Logger 49 | Scheme *runtime.Scheme 50 | } 51 | 52 | // +kubebuilder:rbac:groups=secrets.externalsecret-operator.container-solutions.com,resources=externalsecrets,verbs=get;list;watch;create;update;patch;delete 53 | // +kubebuilder:rbac:groups=secrets.externalsecret-operator.container-solutions.com,resources=externalsecrets/status,verbs=get;update;patch 54 | // +kubebuilder:rbac:groups=store.externalsecret-operator.container-solutions.com,resources=secretstores,verbs=get;list;watch;create;update;patch;delete 55 | // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete 56 | 57 | func (r *ExternalSecretReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 58 | var ( 59 | ctx = context.Background() 60 | log = r.Log.WithValues("externalsecret", req.NamespacedName) 61 | secretLookupName string 62 | refreshInterval time.Duration 63 | ) 64 | 65 | log.Info("Reconciling ExternalSecret") 66 | defer log.Info("Reconcile ExternalSecret Complete") 67 | 68 | // Fetch the ExternalSecret instance 69 | externalSecret := &secretsv1alpha1.ExternalSecret{} 70 | err := r.Get(ctx, req.NamespacedName, externalSecret) 71 | if err != nil { 72 | if errors.IsNotFound(err) { 73 | // Request object not found, could have been deleted after reconcile request. 74 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 75 | // Return and don't requeue 76 | log.Info("External Secret not found.") 77 | return ctrl.Result{}, nil 78 | } 79 | // Error reading the object - requeue the request. 80 | log.Error(err, "Failed to get ExternalSecret") 81 | return ctrl.Result{}, err 82 | } 83 | 84 | refreshInterval, err = r.parseRefreshInterval(externalSecret.Spec.RefreshInterval) 85 | if err != nil { 86 | log.Error(err, "Unable to parse refreshInterval") 87 | return ctrl.Result{}, err 88 | } 89 | 90 | secretStoreRef := externalSecret.Spec.StoreRef 91 | 92 | // Fetch referenced store 93 | secretStore := &storev1alpha1.SecretStore{} 94 | err = r.Get(ctx, types.NamespacedName{Name: secretStoreRef.Name, Namespace: externalSecret.Namespace}, secretStore) 95 | if err != nil { 96 | // Error reading the object - requeue the request. 97 | log.Error(err, "Failed to get SecretStore") 98 | return ctrl.Result{RequeueAfter: defaulRetryPeriod}, err 99 | } 100 | 101 | secretLookupName = externalSecret.Spec.Target.Name 102 | if secretLookupName == "" { 103 | secretLookupName = externalSecret.Name 104 | } 105 | 106 | // Check if this Secret already exists 107 | foundSecret := &corev1.Secret{} 108 | err = r.Get(ctx, types.NamespacedName{Name: secretLookupName, Namespace: externalSecret.Namespace}, foundSecret) 109 | if err != nil { 110 | if errors.IsNotFound(err) { 111 | // Define a new Secret object 112 | secret, err := r.newSecretForCR(externalSecret, secretStore) 113 | if err != nil { 114 | log.Error(err, "Failed to create Secret") 115 | return ctrl.Result{RequeueAfter: defaulRetryPeriod}, err 116 | } 117 | 118 | log.Info("Creating a new Secret", "Secret.Namespace", secret.Namespace, "Secret.Name", secret.Name) 119 | err = r.Create(ctx, secret) 120 | if err != nil { 121 | log.Error(err, "Failed to create Secret", "secret", secret) 122 | return ctrl.Result{}, err 123 | } 124 | 125 | // Secret created successfully - return and requeue after refreshInterval 126 | return ctrl.Result{RequeueAfter: refreshInterval}, nil 127 | } 128 | // Error reading the object - requeue the request. 129 | log.Error(err, "Failed to get Secret") 130 | return ctrl.Result{}, err 131 | } 132 | 133 | // update Secret if it already exists 134 | secretMap, err := r.backendGet(externalSecret, secretStore) 135 | if err != nil { 136 | log.Error(err, "backendGet") 137 | return ctrl.Result{}, err 138 | } 139 | 140 | updateLabels := makeLabels(secretStore.Spec.Controller, externalSecret.Spec.StoreRef.Name) 141 | 142 | foundSecret.ObjectMeta.Labels = updateLabels 143 | foundSecret.Data = secretMap 144 | err = r.Update(ctx, foundSecret) 145 | if err != nil { 146 | log.Error(err, "Failed to update secret") 147 | return ctrl.Result{}, err 148 | } 149 | 150 | return ctrl.Result{RequeueAfter: refreshInterval}, nil 151 | } 152 | 153 | func (r *ExternalSecretReconciler) newSecretForCR(s *secretsv1alpha1.ExternalSecret, st *storev1alpha1.SecretStore) (*corev1.Secret, error) { 154 | var secretObjName string 155 | secretObjName = s.Spec.Target.Name 156 | if secretObjName == "" { 157 | secretObjName = s.Name 158 | } 159 | 160 | secretMap, err := r.backendGet(s, st) 161 | if err != nil { 162 | log.Error(err, "backendGet") 163 | return nil, err 164 | } 165 | 166 | secretLabels := makeLabels(st.Spec.Controller, s.Spec.StoreRef.Name) 167 | 168 | secretObject := &corev1.Secret{ 169 | TypeMeta: metav1.TypeMeta{ 170 | Kind: "Secret", 171 | APIVersion: "v1", 172 | }, 173 | ObjectMeta: metav1.ObjectMeta{ 174 | Name: secretObjName, 175 | Namespace: s.Namespace, 176 | Labels: secretLabels, 177 | }, 178 | Data: secretMap, 179 | } 180 | 181 | // Allows deleted objects to be garbage collected. 182 | err = ctrl.SetControllerReference(s, secretObject, r.Scheme) 183 | if err != nil { 184 | log.Error(err, "Error setting owner references", secretObject) 185 | return nil, err 186 | } 187 | 188 | return secretObject, nil 189 | } 190 | 191 | func (r *ExternalSecretReconciler) backendGet(s *secretsv1alpha1.ExternalSecret, st *storev1alpha1.SecretStore) (map[string][]byte, error) { 192 | secrets := s.Spec.Data 193 | secretMap := make(map[string][]byte) 194 | 195 | stCtrl := st.Spec.Controller 196 | backend, ok := backend.Instances[stCtrl] 197 | if !ok { 198 | log.Error("Cannot find controller:", stCtrl) 199 | return secretMap, fmt.Errorf("Cannot find backend: %v", stCtrl) 200 | } 201 | 202 | for _, secret := range secrets { 203 | retrievedValue, err := backend.Get(secret.Key, secret.Version) 204 | if err != nil { 205 | log.Error(err, "could not create secret due to error from backend") 206 | return secretMap, fmt.Errorf("could not create secret due to error from backend: %v", err) 207 | } 208 | 209 | secretMap[secret.Key] = []byte(retrievedValue) 210 | } 211 | 212 | return secretMap, nil 213 | } 214 | 215 | func (r *ExternalSecretReconciler) parseRefreshInterval(refreshIntervalString string) (time.Duration, error) { 216 | var refreshIntervalValue time.Duration 217 | var err error 218 | 219 | if refreshIntervalString == "" { 220 | refreshIntervalValue = defaultRefreshInterval 221 | } else { 222 | refreshIntervalValue, err = time.ParseDuration(refreshIntervalString) 223 | if err != nil { 224 | log.Error(err, "Unable to parse refreshInterval") 225 | return 0, err 226 | } 227 | } 228 | 229 | return refreshIntervalValue, nil 230 | } 231 | 232 | func makeLabels(contrl string, storeRef string) map[string]string { 233 | return map[string]string{ 234 | "secret-controller": contrl, 235 | "secret-storeRef": storeRef, 236 | } 237 | } 238 | 239 | func (r *ExternalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { 240 | return ctrl.NewControllerManagedBy(mgr). 241 | For(&secretsv1alpha1.ExternalSecret{}). 242 | Owns(&corev1.Secret{}). 243 | Complete(r) 244 | } 245 | -------------------------------------------------------------------------------- /controllers/secrets/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | _ "github.com/containersolutions/externalsecret-operator/pkg/register" 24 | 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | 28 | "k8s.io/client-go/kubernetes/scheme" 29 | "k8s.io/client-go/rest" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/envtest" 33 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 34 | logf "sigs.k8s.io/controller-runtime/pkg/log" 35 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 36 | 37 | secretsv1alpha1 "github.com/containersolutions/externalsecret-operator/apis/secrets/v1alpha1" 38 | storev1alpha1 "github.com/containersolutions/externalsecret-operator/apis/store/v1alpha1" 39 | storecontroller "github.com/containersolutions/externalsecret-operator/controllers/store" 40 | // +kubebuilder:scaffold:imports 41 | ) 42 | 43 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 44 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 45 | 46 | var cfg *rest.Config 47 | var k8sClient client.Client 48 | var testEnv *envtest.Environment 49 | 50 | func TestAPIs(t *testing.T) { 51 | RegisterFailHandler(Fail) 52 | 53 | RunSpecsWithDefaultAndCustomReporters(t, 54 | "Controller Suite", 55 | []Reporter{printer.NewlineReporter{}}) 56 | } 57 | 58 | var _ = BeforeSuite(func(done Done) { 59 | logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 60 | 61 | // customAPIServerFlags := []string{} 62 | 63 | // apiServerFlags := append([]string(nil), envtest.DefaultKubeAPIServerFlags...) 64 | // apiServerFlags = append(apiServerFlags, customAPIServerFlags...) 65 | 66 | // useExistingCluster := true 67 | 68 | By("bootstrapping test environment") 69 | testEnv = &envtest.Environment{ 70 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 71 | // UseExistingCluster: &useExistingCluster, 72 | // AttachControlPlaneOutput: true, 73 | } 74 | 75 | var err error 76 | cfg, err = testEnv.Start() 77 | Expect(err).ToNot(HaveOccurred()) 78 | Expect(cfg).ToNot(BeNil()) 79 | 80 | err = secretsv1alpha1.AddToScheme(scheme.Scheme) 81 | Expect(err).NotTo(HaveOccurred()) 82 | 83 | err = storev1alpha1.AddToScheme(scheme.Scheme) 84 | Expect(err).NotTo(HaveOccurred()) 85 | // +kubebuilder:scaffold:scheme 86 | 87 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 88 | Expect(err).ToNot(HaveOccurred()) 89 | Expect(k8sClient).ToNot(BeNil()) 90 | 91 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 92 | Scheme: scheme.Scheme, 93 | MetricsBindAddress: ":8081", 94 | }) 95 | Expect(err).ToNot(HaveOccurred()) 96 | 97 | err = (&storecontroller.SecretStoreReconciler{ 98 | Client: k8sManager.GetClient(), 99 | Log: ctrl.Log.WithName("controllers").WithName("SecretStore"), 100 | Scheme: k8sManager.GetScheme(), 101 | }).SetupWithManager(k8sManager) 102 | Expect(err).ToNot(HaveOccurred()) 103 | 104 | err = (&ExternalSecretReconciler{ 105 | Client: k8sManager.GetClient(), 106 | Log: ctrl.Log.WithName("controllers").WithName("ExternalSecret"), 107 | Scheme: k8sManager.GetScheme(), 108 | }).SetupWithManager(k8sManager) 109 | Expect(err).ToNot(HaveOccurred()) 110 | 111 | go func() { 112 | defer GinkgoRecover() 113 | err = k8sManager.Start(ctrl.SetupSignalHandler()) 114 | 115 | Expect(err).ToNot(HaveOccurred()) 116 | }() 117 | 118 | k8sClient = k8sManager.GetClient() 119 | Expect(k8sClient).ToNot(BeNil()) 120 | 121 | close(done) 122 | }, 60) 123 | 124 | var _ = AfterSuite(func() { 125 | By("tearing down the test environment") 126 | err := testEnv.Stop() 127 | Expect(err).ToNot(HaveOccurred()) 128 | }) 129 | -------------------------------------------------------------------------------- /controllers/store/secretstore_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | storev1alpha1 "github.com/containersolutions/externalsecret-operator/apis/store/v1alpha1" 24 | "github.com/go-logr/logr" 25 | "k8s.io/apimachinery/pkg/api/errors" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/types" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | 31 | corev1 "k8s.io/api/core/v1" 32 | // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 | 34 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 35 | config "github.com/containersolutions/externalsecret-operator/pkg/config" 36 | ) 37 | 38 | const ( 39 | defaulRetryPeriod = time.Second * 30 40 | ) 41 | 42 | // SecretStoreReconciler reconciles a SecretStore object 43 | type SecretStoreReconciler struct { 44 | client.Client 45 | Log logr.Logger 46 | Scheme *runtime.Scheme 47 | } 48 | 49 | // +kubebuilder:rbac:groups=store.externalsecret-operator.container-solutions.com,resources=secretstores,verbs=get;list;watch;create;update;patch;delete 50 | // +kubebuilder:rbac:groups=store.externalsecret-operator.container-solutions.com,resources=secretstores/status,verbs=get;update;patch 51 | // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete 52 | 53 | func (r *SecretStoreReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 54 | ctx := context.Background() 55 | log := r.Log.WithValues("secretstore", req.NamespacedName) 56 | 57 | log.Info("Reconciling SecretStore") 58 | defer log.Info("Reconcile SecretStore Complete") 59 | 60 | // Fetch the SecretStore instance 61 | secretStore := &storev1alpha1.SecretStore{} 62 | err := r.Get(ctx, req.NamespacedName, secretStore) 63 | if err != nil { 64 | if errors.IsNotFound(err) { 65 | // Request object not found, could have been deleted after reconcile request. 66 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 67 | // Return and don't requeue 68 | log.Info("SecretStore not found") 69 | return ctrl.Result{}, nil 70 | } 71 | // Error reading the object - requeue the request. 72 | log.Error(err, "Failed to get SecretStore") 73 | return ctrl.Result{}, err 74 | } 75 | 76 | contrl := secretStore.Spec.Controller 77 | 78 | storeConfig := secretStore.Spec.Store.Raw 79 | 80 | config, err := config.ConfigFromCtrl(storeConfig) 81 | if err != nil { 82 | return ctrl.Result{}, err 83 | } 84 | 85 | secretRef := config.Auth["secretRef"].(map[string]interface{}) 86 | secretRefName := secretRef["name"].(string) 87 | 88 | // Fetch credential Secret 89 | credentialsSecret := &corev1.Secret{} 90 | err = r.Get(ctx, types.NamespacedName{Name: secretRefName, Namespace: secretStore.Namespace}, credentialsSecret) 91 | if err != nil { 92 | // Error reading the object - requeue the request. 93 | log.Error(err, "Failed to get credentials Secret") 94 | return ctrl.Result{RequeueAfter: defaulRetryPeriod}, err 95 | } 96 | 97 | credentials := credentialsSecret.Data["credentials.json"] 98 | 99 | err = backend.InitFromCtrl(contrl, config, credentials) 100 | if err != nil { 101 | log.Error(err, "Backend initialization failed") 102 | return ctrl.Result{}, err 103 | } 104 | 105 | return ctrl.Result{}, nil 106 | } 107 | 108 | func (r *SecretStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { 109 | return ctrl.NewControllerManagedBy(mgr). 110 | For(&storev1alpha1.SecretStore{}). 111 | Complete(r) 112 | } 113 | -------------------------------------------------------------------------------- /controllers/store/secretstore_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | storev1alpha1 "github.com/containersolutions/externalsecret-operator/apis/store/v1alpha1" 8 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 9 | "github.com/containersolutions/externalsecret-operator/pkg/utils" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | 14 | corev1 "k8s.io/api/core/v1" 15 | "k8s.io/apimachinery/pkg/api/errors" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/apimachinery/pkg/types" 19 | ) 20 | 21 | const SecretStoreNamespace = "default" 22 | 23 | var _ = Describe("SecretstoreController", func() { 24 | var ( 25 | SecretStoreName = "externalsecret-operator-store-test" 26 | SecretStoreControllerName = "test-store-ctrl" 27 | KeyName = "test-store-secret" 28 | KeyVersion = "test-store-version" 29 | CredentialSecretName = "credential-secret-store" 30 | 31 | timeout = time.Second * 30 32 | // duration = time.Second * 10 33 | interval = time.Millisecond * 250 34 | 35 | storeConfig = ` 36 | { 37 | "type": "dummy", 38 | "auth": { 39 | "secretRef": { 40 | "name": "credential-secret-store" 41 | } 42 | }, 43 | "parameters": { 44 | "Suffix": "TestParameter" 45 | } 46 | }` 47 | ) 48 | 49 | Context("When creating a SecretStore", func() { 50 | ctx := context.Background() 51 | It("Should intialize backend with the the given controller name", func() { 52 | 53 | credentialsSecret := &corev1.Secret{ 54 | ObjectMeta: metav1.ObjectMeta{ 55 | Name: CredentialSecretName, 56 | Namespace: SecretStoreNamespace, 57 | }, 58 | StringData: map[string]string{ 59 | "credentials.json": `{ 60 | "Credential": "-dummyvalue" 61 | }`, 62 | }, 63 | } 64 | Expect(k8sClient.Create(ctx, credentialsSecret)).Should(Succeed()) 65 | 66 | credentialsSecretLookupKey := types.NamespacedName{Name: CredentialSecretName, Namespace: SecretStoreNamespace} 67 | createdCredentialsSecret := &corev1.Secret{} 68 | 69 | Eventually(func() bool { 70 | err := k8sClient.Get(ctx, credentialsSecretLookupKey, createdCredentialsSecret) 71 | if err != nil { 72 | return false 73 | } 74 | return true 75 | }, timeout, interval).Should(BeTrue()) 76 | 77 | secretStore := &storev1alpha1.SecretStore{} 78 | 79 | secretStore.ObjectMeta = metav1.ObjectMeta{ 80 | Name: SecretStoreName, 81 | Namespace: SecretStoreNamespace, 82 | } 83 | 84 | secretStore.Spec = storev1alpha1.SecretStoreSpec{ 85 | Controller: SecretStoreControllerName, 86 | Store: runtime.RawExtension{ 87 | Raw: []byte(storeConfig), 88 | }, 89 | } 90 | 91 | Expect(k8sClient.Create(ctx, secretStore)).Should(Succeed()) 92 | 93 | secretStoreLookupKey := types.NamespacedName{Name: SecretStoreName, Namespace: SecretStoreNamespace} 94 | createdSecretStore := &storev1alpha1.SecretStore{} 95 | 96 | Eventually(func() bool { 97 | err := k8sClient.Get(ctx, secretStoreLookupKey, createdSecretStore) 98 | if err != nil { 99 | return false 100 | } 101 | return true 102 | }, timeout, interval).Should(BeTrue()) 103 | 104 | Expect(createdSecretStore.Spec.Controller).To(Equal(SecretStoreControllerName)) 105 | 106 | Eventually(func() bool { 107 | _, found := backend.Instances[SecretStoreControllerName] 108 | 109 | return found 110 | }, timeout, interval).Should(BeTrue()) 111 | 112 | Eventually(func() string { 113 | backend := backend.Instances[SecretStoreControllerName] 114 | if backend == nil { 115 | return "" 116 | } 117 | secretValue, err := backend.Get(KeyName, KeyVersion) 118 | if err != nil { 119 | return "" 120 | } 121 | return secretValue 122 | }, timeout, interval).Should(Equal("test-store-secrettest-store-versionTestParameter")) 123 | 124 | By("Deleting the SecretStore") 125 | Eventually(func() error { 126 | ss := &storev1alpha1.SecretStore{} 127 | k8sClient.Get(context.Background(), secretStoreLookupKey, ss) 128 | return k8sClient.Delete(context.Background(), ss) 129 | }, timeout, interval).Should(Succeed()) 130 | 131 | Eventually(func() error { 132 | ss := &storev1alpha1.SecretStore{} 133 | return k8sClient.Get(context.Background(), secretStoreLookupKey, ss) 134 | }, timeout, interval).ShouldNot(Succeed()) 135 | }) 136 | }) 137 | 138 | Context("When creating a SecretStore", func() { 139 | ctx := context.Background() 140 | 141 | It("Should handle a missing secret store gracefully", func() { 142 | randomObjSafeStr, err := utils.RandomStringObjectSafe(30) 143 | Expect(err).To(BeNil()) 144 | randomSecretName := "Non existernt Secret Store" + randomObjSafeStr 145 | 146 | secretStoreLookupKey := types.NamespacedName{Name: randomSecretName, Namespace: SecretStoreNamespace} 147 | nonExistentSecretStore := &storev1alpha1.SecretStore{} 148 | 149 | err = k8sClient.Get(ctx, secretStoreLookupKey, nonExistentSecretStore) 150 | 151 | Expect(err).ToNot(BeNil()) 152 | Expect(errors.IsNotFound(err)).To(BeTrue()) 153 | }) 154 | }) 155 | 156 | Context("When creating a SecretStore then", func() { 157 | ctx := context.Background() 158 | 159 | storeConfig2 := ` 160 | { 161 | "type": "dummy", 162 | "auth": { 163 | "secretRef": { 164 | "name": "credential-secret-non-existent" 165 | } 166 | }, 167 | "parameters": { 168 | "Suffix": "TestParameter" 169 | } 170 | }` 171 | 172 | It("Should handle a missing credential secret", func() { 173 | randomObjSafeStr, err := utils.RandomStringObjectSafe(30) 174 | Expect(err).To(BeNil()) 175 | randomSecretStoreName := SecretStoreName + randomObjSafeStr 176 | 177 | secretStore := &storev1alpha1.SecretStore{} 178 | 179 | secretStore.ObjectMeta = metav1.ObjectMeta{ 180 | Name: randomSecretStoreName, 181 | Namespace: SecretStoreNamespace, 182 | } 183 | 184 | secretStore.Spec = storev1alpha1.SecretStoreSpec{ 185 | Controller: SecretStoreControllerName, 186 | Store: runtime.RawExtension{ 187 | Raw: []byte(storeConfig2), 188 | }, 189 | } 190 | 191 | Expect(k8sClient.Create(ctx, secretStore)).Should(Succeed()) 192 | 193 | secretStoreLookupKey := types.NamespacedName{Name: randomSecretStoreName, Namespace: SecretStoreNamespace} 194 | createdSecretStore := &storev1alpha1.SecretStore{} 195 | 196 | Eventually(func() bool { 197 | err := k8sClient.Get(ctx, secretStoreLookupKey, createdSecretStore) 198 | if err != nil { 199 | return false 200 | } 201 | return true 202 | }, timeout, interval).Should(BeTrue()) 203 | 204 | Expect(createdSecretStore.Spec.Controller).To(Equal(SecretStoreControllerName)) 205 | 206 | }) 207 | }) 208 | 209 | Context("When creating a SecretStore", func() { 210 | ctx := context.Background() 211 | // blank params trigger error during dummy Init() 212 | storeConfig3 := ` 213 | { 214 | "type": "dummy", 215 | "auth": { 216 | "secretRef": { 217 | "name": "credential-secret-store" 218 | } 219 | }, 220 | "parameters": {} 221 | }` 222 | 223 | It("Should handle Init() failure", func() { 224 | randomObjSafeStr, err := utils.RandomStringObjectSafe(35) 225 | Expect(err).To(BeNil()) 226 | randomSecretStoreName := SecretStoreName + randomObjSafeStr 227 | 228 | secretStore := &storev1alpha1.SecretStore{} 229 | 230 | secretStore.ObjectMeta = metav1.ObjectMeta{ 231 | Name: randomSecretStoreName, 232 | Namespace: SecretStoreNamespace, 233 | } 234 | 235 | secretStore.Spec = storev1alpha1.SecretStoreSpec{ 236 | Controller: SecretStoreControllerName, 237 | Store: runtime.RawExtension{ 238 | Raw: []byte(storeConfig3), 239 | }, 240 | } 241 | 242 | Expect(k8sClient.Create(ctx, secretStore)).Should(Succeed()) 243 | }) 244 | }) 245 | }) 246 | -------------------------------------------------------------------------------- /controllers/store/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | _ "github.com/containersolutions/externalsecret-operator/pkg/register" 24 | 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/rest" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest" 32 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 33 | logf "sigs.k8s.io/controller-runtime/pkg/log" 34 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 35 | 36 | storev1alpha1 "github.com/containersolutions/externalsecret-operator/apis/store/v1alpha1" 37 | // +kubebuilder:scaffold:imports 38 | ) 39 | 40 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 41 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 42 | 43 | var cfg *rest.Config 44 | var k8sClient client.Client 45 | var testEnv *envtest.Environment 46 | 47 | func TestAPIs(t *testing.T) { 48 | RegisterFailHandler(Fail) 49 | 50 | RunSpecsWithDefaultAndCustomReporters(t, 51 | "Controller Suite", 52 | []Reporter{printer.NewlineReporter{}}) 53 | } 54 | 55 | var _ = BeforeSuite(func(done Done) { 56 | logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 57 | 58 | // customAPIServerFlags := []string{} 59 | 60 | // apiServerFlags := append([]string(nil), envtest.DefaultKubeAPIServerFlags...) 61 | // apiServerFlags = append(apiServerFlags, customAPIServerFlags...) 62 | 63 | // useExistingCluster := true 64 | 65 | By("bootstrapping test environment") 66 | testEnv = &envtest.Environment{ 67 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 68 | // UseExistingCluster: &useExistingCluster, 69 | // AttachControlPlaneOutput: true 70 | } 71 | 72 | var err error 73 | cfg, err = testEnv.Start() 74 | Expect(err).ToNot(HaveOccurred()) 75 | Expect(cfg).ToNot(BeNil()) 76 | 77 | err = storev1alpha1.AddToScheme(scheme.Scheme) 78 | Expect(err).NotTo(HaveOccurred()) 79 | 80 | // +kubebuilder:scaffold:scheme 81 | 82 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 83 | Expect(err).ToNot(HaveOccurred()) 84 | Expect(k8sClient).ToNot(BeNil()) 85 | 86 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 87 | Scheme: scheme.Scheme, 88 | MetricsBindAddress: ":8080", 89 | }) 90 | Expect(err).ToNot(HaveOccurred()) 91 | 92 | err = (&SecretStoreReconciler{ 93 | Client: k8sManager.GetClient(), 94 | Log: ctrl.Log.WithName("controllers").WithName("SecretStore"), 95 | Scheme: k8sManager.GetScheme(), 96 | }).SetupWithManager(k8sManager) 97 | Expect(err).ToNot(HaveOccurred()) 98 | 99 | go func() { 100 | defer GinkgoRecover() 101 | err = k8sManager.Start(ctrl.SetupSignalHandler()) 102 | 103 | Expect(err).ToNot(HaveOccurred()) 104 | }() 105 | 106 | k8sClient = k8sManager.GetClient() 107 | Expect(k8sClient).ToNot(BeNil()) 108 | 109 | close(done) 110 | }, 60) 111 | 112 | var _ = AfterSuite(func() { 113 | By("tearing down the test environment") 114 | err := testEnv.Stop() 115 | Expect(err).ToNot(HaveOccurred()) 116 | }) 117 | -------------------------------------------------------------------------------- /docs/backends/akv.md: -------------------------------------------------------------------------------- 1 | ## Azure Key Vault secrets 2 | 3 | ### Prerequisites 4 | 5 | You need to have a Key Vault instance with a secret and an application registered in your Azure Active directory with Read access to the Vault's secrets. 6 | 7 | The following script creates everything needed for sample purposes. It assumes you have Azure ClI installed and it is already authenticated. 8 | For more information about Azure CLI refer to the Azure CLI's [documentation page](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli). 9 | 10 | ```bash 11 | # The tennatId will be needed to get the secrets 12 | TENANT_ID=$(az account show --query tenantId | tr -d \") 13 | 14 | # Name and location of the Resource Group 15 | RESOURCE_GROUP="MyKeyVaultResourceGroup" 16 | LOCATION="westus" 17 | 18 | # Create the Resource Group 19 | az group create --location $LOCATION --name $RESOURCE_GROUP 20 | 21 | VAULT_NAME="eso-akv-test" 22 | 23 | # Create the Key Vault 24 | az keyvault create --name $VAULT_NAME --resource-group $RESOURCE_GROUP 25 | 26 | SECRET_NAME="example-externalsecret-key" 27 | SECRET_VAlUE="This is our secret now!" 28 | 29 | # Add a secret to the vault 30 | az keyvault secret set --name $SECRET_NAME --vault-name $VAULT_NAME --value "$SECRET_VAlUE" 31 | 32 | # Now you need to create an app to access the Key Vault 33 | APP_NAME="ExtSectret Query App" 34 | APP_ID=$(az ad app create --display-name "$APP_NAME" --query appId | tr -d \") 35 | 36 | # A Service Principal must also be created 37 | SERVICE_PRINCIPAL=$(az ad sp create --id $APP_ID --query objectId | tr -d \") 38 | 39 | # Add permission to your App to query the Key Vault 40 | # The --api-permission refers to the Azure Key Vault user_impersonation permission (do not modify) 41 | # The --api refers to the Azure Key Vault API (do not modify) 42 | az ad app permission add --id $APP_ID --api-permissions f53da476-18e3-4152-8e01-aec403e6edc0=Scope --api cfa8b339-82a2-471a-a3c9-0fc0be7a4093 43 | 44 | APP_PASSWORD="ThisisMyStrongPassword" 45 | # A password must be created for the app 46 | az ad app credential reset --id $APP_ID --password "$APP_PASSWORD" 47 | 48 | # Finnaly, the Key Vault must have an Access Policy for the created app 49 | az keyvault set-policy --name $VAULT_NAME --object-id $SERVICE_PRINCIPAL --secret-permissions get 50 | ``` 51 | 52 | For a detailed view on how to create the above mentioned resources in the Azure Portal, please go to: [How To Access Azure Key Vault Secrets Through Rest API Using Postman](https://www.c-sharpcorner.com/article/how-to-access-azure-key-vault-secrets-through-rest-api-using-postman/) 53 | 54 | - Now you're ready to tnstall CRDs 55 | ``` 56 | make install 57 | ``` 58 | 59 | ### Deployment 60 | 61 | - Uncomment and update credentials to be used in `config/credentials/kustomization.yaml`: 62 | 63 | ```yaml 64 | resources: 65 | # - credentials-gsm.yaml 66 | # - credentials-asm.yaml 67 | # - credentials-dummy.yaml 68 | # - credentials-gitlab.yaml 69 | - credentials-akv.yaml 70 | 71 | ``` 72 | 73 | - Update the Azure Key Vault backend credentials `config/credentials/credentials-akv.yaml` with your personal access token 74 | ```json 75 | { 76 | "tenantId": "", 77 | "clientId": "", 78 | "clientSecret": "", 79 | "keyvault": "" 80 | } 81 | ``` 82 | 83 | You can run the following script that will generate the above mentioned json object 84 | ```bash 85 | echo -e "{ \n \ 86 | \"tenantId\": \"$TENANT_ID\", \n \ 87 | \"clientId\": \"$APP_ID\", \n \ 88 | \"clientSecret\": \"$APP_PASSWORD\", \n \ 89 | \"keyvault\": \"$VAULT_NAME\" \n \ 90 | }" 91 | ``` 92 | > Beware of the indentation if you paste the output from above into your file. 93 | 94 | 95 | - Update the `SecretStore` resource definition `config/samples/store_v1alpha1_secretstore.yaml` 96 | ```yaml 97 | % cat `config/samples/store_v1alpha1_secretstore.yaml 98 | apiVersion: store.externalsecret-operator.container-solutions.com/v1alpha1 99 | kind: SecretStore 100 | metadata: 101 | name: secretstore-sample 102 | spec: 103 | controller: staging 104 | store: 105 | type: akv 106 | auth: 107 | secretRef: 108 | name: externalsecret-operator-credentials-akv 109 | parameters: {} 110 | ``` 111 | 112 | - Update the `ExternalSecret` resource definition `config/samples/secrets_v1alpha1_externalsecret.yaml` 113 | ```yaml 114 | % cat config/samples/secrets_v1alpha1_externalsecret.yaml 115 | apiVersion: secrets.externalsecret-operator.container-solutions.com/v1alpha1 116 | kind: ExternalSecret 117 | metadata: 118 | name: externalsecret-sample 119 | spec: 120 | storeRef: 121 | name: externalsecret-operator-secretstore-sample 122 | data: 123 | - key: example-externalsecret-key 124 | version: "" 125 | ``` 126 | 127 | - The operator fetches the Key Vault secret from Azure and injects it as a Kubernetes secret: 128 | 129 | ```shell 130 | % make deploy 131 | % kubectl get secret externalsecret-operator-externalsecret-sample -n externalsecret-operator-system \ 132 | -o jsonpath='{.data.example-externalsecret-key}' | base64 -d 133 | ``` 134 | 135 | ### Clean up 136 | 137 | - Delete the resource group (it willa lso delete the Kay Vault created) 138 | 139 | ```bash 140 | az group delete --name $RESOURCE_GROUP 141 | ``` 142 | 143 | - Delete the Active Directory application 144 | 145 | ```bash 146 | az ad app delete --id $APP_ID 147 | ``` -------------------------------------------------------------------------------- /docs/backends/credstash.md: -------------------------------------------------------------------------------- 1 | ## Credstash (AWS KMS) 2 | 3 | #### Prerequisites 4 | 5 | Create a KMS key in IAM, using an aws profile you have configured in the aws CLI. You can ommit --profile if you use the Default profile. 6 | 7 | ``` 8 | aws --region ap-southeast-2 --profile [yourawsprofile] kms create-key --query 'KeyMetadata.KeyId' 9 | ``` 10 | 11 | Assign the credstash alias to the key using the key id printed when you created the KMS key. 12 | 13 | ``` 14 | aws --region ap-southeast-2 --profile [yourawsprofile] kms create-alias --alias-name 'alias/credstash' --target-key-id "xxxx-xxxx-xxxx-xxx 15 | ``` 16 | 17 | Use a credstash client to create a secret (Using security context securityKey=securityValue here). 18 | 19 | ``` 20 | credstash put example-externalsecret-key secretValue securityKey=securityValue 21 | ``` 22 | 23 | 24 | - Install CRDs 25 | ``` 26 | make install 27 | ``` 28 | 29 | #### Deployment 30 | 31 | - Uncomment and update credentials to be used in `config/credentials/kustomization.yaml`: 32 | 33 | ```yaml 34 | resources: 35 | # - credentials-gsm.yaml 36 | # - credentials-asm.yaml 37 | # - credentials-dummy.yaml 38 | # - credentials-gitlab.yaml 39 | - credentials-credstash.yaml 40 | ``` 41 | 42 | - Update the credstash credentials `config/credentials/credentials-credstash.yaml` with correct AWS credentials. 43 | 44 | - Update the `SecretStore` resource definition `config/samples/store_v1alpha1_secretstore.yaml` 45 | 46 | ```yaml 47 | % cat `config/samples/store_v1alpha1_secretstore.yaml 48 | apiVersion: store.externalsecret-operator.container-solutions.com/v1alpha1 49 | kind: SecretStore 50 | metadata: 51 | name: secretstore-sample 52 | spec: 53 | controller: staging 54 | store: 55 | type: credstash 56 | auth: 57 | secretRef: 58 | name: externalsecret-operator-credentials-credstash 59 | parameters: 60 | region: eu-west-2 61 | ``` 62 | 63 | - Update the `ExternalSecret` resource definition `config/samples/secrets_v1alpha1_externalsecret.yaml` 64 | ```yaml 65 | % cat config/samples/secrets_v1alpha1_externalsecret.yaml 66 | apiVersion: secrets.externalsecret-operator.container-solutions.com/v1alpha1 67 | kind: ExternalSecret 68 | metadata: 69 | name: externalsecret-sample 70 | spec: 71 | storeRef: 72 | name: externalsecret-operator-secretstore-sample 73 | data: 74 | - key: example-externalsecret-key 75 | version: latest 76 | ``` 77 | 78 | - The operator fetches the secret from AWS KMS like a credstash client 79 | secret: 80 | 81 | ```shell 82 | % make deploy 83 | % kubectl get secret externalsecret-operator-externalsecret-sample -n externalsecret-operator-system \ 84 | -o jsonpath='{.data.example-externalsecret-key}' | base64 -d 85 | ``` -------------------------------------------------------------------------------- /docs/backends/gitlab.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Gitlab CI/CD Variables 4 | 5 | 6 | 7 | #### Prerequisites 8 | - A Gitlab project with a CI/CD variable, who's key is `example_externalsecret_key` 9 | - The project ID which you can find at the top of the main page of the project, right below the project name. 10 | - A [Gitlab personal access token](https://gitlab.com/-/profile/personal_access_tokens) with `read_api` permissions 11 | 12 | - Install CRDs 13 | ``` 14 | make install 15 | ``` 16 | 17 | 18 | 19 | #### Deployment 20 | 21 | - Uncomment and update credentials to be used in `config/credentials/kustomization.yaml`: 22 | 23 | ```yaml 24 | resources: 25 | # - credentials-gsm.yaml 26 | # - credentials-asm.yaml 27 | # - credentials-dummy.yaml 28 | - credentials-gitlab.yaml 29 | ``` 30 | 31 | - Update the gitlab credentials `config/credentials/credentials-gitlab.yaml` with your personal access token 32 | 33 | ```yaml 34 | %cat config/credentials/credentials-gitlab.yaml 35 | ... 36 | credentials.json: |- 37 | { 38 | "token": "abcdef12345" 39 | } 40 | 41 | ``` 42 | - Update the `SecretStore` resource definition `config/samples/store_v1alpha1_secretstore.yaml` 43 | ```yaml 44 | % cat `config/samples/store_v1alpha1_secretstore.yaml 45 | apiVersion: store.externalsecret-operator.container-solutions.com/v1alpha1 46 | kind: SecretStore 47 | metadata: 48 | name: secretstore-sample 49 | spec: 50 | controller: staging 51 | store: 52 | type: gitlab 53 | auth: 54 | secretRef: 55 | name: externalsecret-operator-credentials-gitlab 56 | parameters: 57 | baseURL: https://gitlab.com 58 | projectID: 12345678 59 | ``` 60 | 61 | - Update the `ExternalSecret` resource definition `config/samples/secrets_v1alpha1_externalsecret.yaml` 62 | ```yaml 63 | % cat config/samples/secrets_v1alpha1_externalsecret.yaml 64 | apiVersion: secrets.externalsecret-operator.container-solutions.com/v1alpha1 65 | kind: ExternalSecret 66 | metadata: 67 | name: externalsecret-sample 68 | spec: 69 | storeRef: 70 | name: externalsecret-operator-secretstore-sample 71 | data: 72 | - key: example_externalsecret_key 73 | version: latest 74 | ``` 75 | 76 | - The operator fetches the CI/CD variable from Gitlab and injects it as a secret: 77 | 78 | ```shell 79 | % make deploy 80 | % kubectl get secret externalsecret-operator-externalsecret-sample -n externalsecret-operator-system \ 81 | -o jsonpath='{.data.example_externalsecret_key}' | base64 -d 82 | ``` -------------------------------------------------------------------------------- /docs/backends/gsm.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## GCP Secret Manager 4 | 5 | 6 | 7 | #### Prerequisites 8 | - Enabled and configured secret manager API on your GCP project. [Secret Manager Docs](https://cloud.google.com/secret-manager/docs/configuring-secret-manager) 9 | 10 | - Install CRDs 11 | ``` 12 | make install 13 | ``` 14 | 15 | 16 | 17 | #### Deployment 18 | 19 | - Uncomment and update credentials to be used in `config/credentials/kustomization.yaml`: 20 | 21 | ```yaml 22 | resources: 23 | - credentials-gsm.yaml 24 | # - credentials-asm.yaml 25 | # - credentials-dummy.yaml 26 | # - credentials-gitlab.yaml 27 | ``` 28 | 29 | - Update the gsm credentials `config/credentials/credentials-gsm.yaml` with service account key JSON 30 | 31 | ```yaml 32 | %cat config/credentials/credentials-gsm.yaml 33 | ... 34 | credentials.json: |- 35 | { 36 | "type": "service_account" 37 | .... 38 | } 39 | 40 | ``` 41 | - Update the `SecretStore` resource definition `config/samples/store_v1alpha1_secretstore.yaml` 42 | ```yaml 43 | % cat `config/samples/store_v1alpha1_secretstore.yaml 44 | apiVersion: store.externalsecret-operator.container-solutions.com/v1alpha1 45 | kind: SecretStore 46 | metadata: 47 | name: secretstore-sample 48 | spec: 49 | controller: staging 50 | store: 51 | type: gsm 52 | auth: 53 | secretRef: 54 | name: externalsecret-operator-credentials-gsm 55 | parameters: 56 | projectID: external-secrets-operator 57 | ``` 58 | 59 | - Update the `ExternalSecret` resource definition `config/samples/secrets_v1alpha1_externalsecret.yaml` 60 | ```yaml 61 | % cat config/samples/secrets_v1alpha1_externalsecret.yaml 62 | apiVersion: secrets.externalsecret-operator.container-solutions.com/v1alpha1 63 | kind: ExternalSecret 64 | metadata: 65 | name: externalsecret-sample 66 | spec: 67 | storeRef: 68 | name: externalsecret-operator-secretstore-sample 69 | data: 70 | - key: example-externalsecret-key 71 | version: latest 72 | ``` 73 | 74 | - The operator fetches the secret from GCP Secret Manager and injects it as a 75 | secret: 76 | 77 | ```shell 78 | % make deploy 79 | % kubectl get secret externalsecret-operator-externalsecret-sample -n externalsecret-operator-system \ 80 | -o jsonpath='{.data.example-externalsecret-key}' | base64 -d 81 | ``` -------------------------------------------------------------------------------- /docs/spec/ExternalSecret.md: -------------------------------------------------------------------------------- 1 | ``` 2 | apiVersion: secrets.externalsecret-operator.container-solutions.com/v1alpha1 3 | kind: ExternalSecret 4 | metadata: {...} 5 | spec: 6 | # Optional 7 | # The amount of time before the values will be read again from the store 8 | # Secret Rotation Period; 9 | # Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". 10 | refreshInterval: [String] - Default value "1h" 11 | 12 | # Secret name to be created by ExternalSecret 13 | # Optional 14 | target: 15 | # The secret name of the resource 16 | # defaults to .metadata.name of the ExternalSecret. immutable. 17 | name: my-secret 18 | 19 | # Required 20 | # A reference to the store used to fetch the secrets 21 | storeRef: 22 | kind: SecretStore # ClusterSecretStore 23 | name: my-store 24 | 25 | # Required 26 | # data contains key/value pairs which correspond to the keys in the resulting secret 27 | data: [Array] 28 | - key: [String] 29 | version: [String] 30 | 31 | status: {} 32 | ``` -------------------------------------------------------------------------------- /docs/spec/SecretStore.md: -------------------------------------------------------------------------------- 1 | ``` 2 | apiVerson: store.externalsecret-operator.container-solutions.com/v1alpha1 3 | kind: SecretStore 4 | metadata: {...} 5 | spec: 6 | 7 | # Required 8 | # Unique name used to differenciate between different environments i.e production-aws, staging-aws, development- 9 | # NOTE: It should be unique for each store to avoid issues! 10 | controller: "dev" 11 | 12 | # Required 13 | store: 14 | # Sample store types 15 | # AWS Secrets Manager 16 | # store: 17 | # type: asm 18 | # auth: 19 | # secretRef: 20 | # name: externalsecret-operator-credentials-asm 21 | # parameters: 22 | # region: eu-west-2 23 | 24 | # GCP Secret Manager 25 | # store: 26 | # type: gsm 27 | # auth: 28 | # secretRef: 29 | # name: externalsecret-operator-credentials-gsm 30 | # parameters: 31 | # projectID: external-secrets-operator 32 | 33 | status: {} 34 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/containersolutions/externalsecret-operator 2 | 3 | go 1.15 4 | 5 | require ( 6 | cloud.google.com/go v0.66.0 7 | github.com/Azure/azure-sdk-for-go v48.2.0+incompatible 8 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.3 // indirect 9 | github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect 10 | github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect 11 | github.com/apex/log v1.9.0 12 | github.com/aws/aws-sdk-go v1.34.29 13 | github.com/go-logr/logr v0.2.1 14 | github.com/go-logr/zapr v0.2.0 // indirect 15 | github.com/googleapis/gax-go v1.0.3 16 | github.com/onsi/ginkgo v1.14.1 17 | github.com/onsi/gomega v1.10.2 18 | github.com/prometheus/common v0.13.0 19 | github.com/smartystreets/goconvey v1.6.4 20 | github.com/versent/unicreds v1.5.1-0.20180327234242-7135c859e003 21 | github.com/xanzy/go-gitlab v0.39.0 22 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 23 | google.golang.org/api v0.32.0 24 | google.golang.org/genproto v0.0.0-20200921165018-b9da36f5f452 25 | google.golang.org/grpc v1.31.1 26 | k8s.io/api v0.19.2 27 | k8s.io/apimachinery v0.19.2 28 | k8s.io/client-go v0.19.2 29 | sigs.k8s.io/controller-runtime v0.6.3 30 | ) 31 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | _ "github.com/containersolutions/externalsecret-operator/pkg/register" 24 | 25 | "k8s.io/apimachinery/pkg/runtime" 26 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 27 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 28 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 31 | 32 | secretsv1alpha1 "github.com/containersolutions/externalsecret-operator/apis/secrets/v1alpha1" 33 | storev1alpha1 "github.com/containersolutions/externalsecret-operator/apis/store/v1alpha1" 34 | secretscontroller "github.com/containersolutions/externalsecret-operator/controllers/secrets" 35 | storecontroller "github.com/containersolutions/externalsecret-operator/controllers/store" 36 | // +kubebuilder:scaffold:imports 37 | ) 38 | 39 | var ( 40 | scheme = runtime.NewScheme() 41 | setupLog = ctrl.Log.WithName("setup") 42 | ) 43 | 44 | func init() { 45 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 46 | 47 | utilruntime.Must(secretsv1alpha1.AddToScheme(scheme)) 48 | utilruntime.Must(storev1alpha1.AddToScheme(scheme)) 49 | // +kubebuilder:scaffold:scheme 50 | } 51 | 52 | func main() { 53 | var metricsAddr string 54 | var enableLeaderElection bool 55 | // var LeaderElectionID = "36af4962.externalsecret-operator.container-solutions.com" 56 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 57 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 58 | "Enable leader election for controller manager. "+ 59 | "Enabling this will ensure there is only one active controller manager.") 60 | flag.Parse() 61 | 62 | ctrl.SetLogger(zap.New(zap.UseDevMode(true))) 63 | 64 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 65 | Scheme: scheme, 66 | MetricsBindAddress: metricsAddr, 67 | Port: 9443, 68 | LeaderElection: enableLeaderElection, 69 | LeaderElectionID: "36af4962.externalsecret-operator.container-solutions.com", 70 | }) 71 | if err != nil { 72 | setupLog.Error(err, "unable to start manager") 73 | os.Exit(1) 74 | } 75 | 76 | if err = (&storecontroller.SecretStoreReconciler{ 77 | Client: mgr.GetClient(), 78 | Log: ctrl.Log.WithName("controllers").WithName("SecretStore"), 79 | Scheme: mgr.GetScheme(), 80 | }).SetupWithManager(mgr); err != nil { 81 | setupLog.Error(err, "unable to create controller", "controller", "SecretStore") 82 | os.Exit(1) 83 | } 84 | 85 | if err = (&secretscontroller.ExternalSecretReconciler{ 86 | Client: mgr.GetClient(), 87 | Log: ctrl.Log.WithName("controllers").WithName("ExternalSecret"), 88 | Scheme: mgr.GetScheme(), 89 | }).SetupWithManager(mgr); err != nil { 90 | setupLog.Error(err, "unable to create controller", "controller", "ExternalSecret") 91 | os.Exit(1) 92 | } 93 | 94 | // +kubebuilder:scaffold:builder 95 | 96 | setupLog.Info("starting manager") 97 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 98 | setupLog.Error(err, "problem running manager") 99 | os.Exit(1) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/akv/backend.go: -------------------------------------------------------------------------------- 1 | // Package akv implements backend for Azure Key Vault secrets 2 | package akv 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | 12 | "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" 13 | kvauth "github.com/Azure/azure-sdk-for-go/services/keyvault/auth" 14 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | ) 17 | 18 | var log = ctrl.Log.WithName("akv") 19 | 20 | type ClientInterface interface { 21 | GetSecret(context context.Context, url string, key string, version string) (keyvault.SecretBundle, error) 22 | } 23 | 24 | // Backend represents a backend for Azure Key Vault 25 | type Backend struct { 26 | Client ClientInterface 27 | keyvault string 28 | } 29 | 30 | // NewBackend returns an uninitialized Backend for AWS Secret Manager 31 | func NewBackend() backend.Backend { 32 | return &Backend{} 33 | } 34 | 35 | func init() { 36 | backend.Register("akv", NewBackend) 37 | } 38 | 39 | // Init initializes the Backend for Azure Key Vault 40 | func (a *Backend) Init(parameters map[string]interface{}, credentials []byte) error { 41 | 42 | akvCred := AzureCredentials{} 43 | err := json.Unmarshal(credentials, &akvCred) 44 | if err != nil { 45 | log.Error(err, "") 46 | return err 47 | } 48 | 49 | file, err := ioutil.TempFile("/tmp", "akv") 50 | if err != nil { 51 | log.Error(err, "") 52 | return err 53 | } 54 | 55 | _, err = file.Write(credentials) 56 | if err != nil { 57 | log.Error(err, "error writing the temp file") 58 | return err 59 | } 60 | 61 | if err := os.Setenv("AZURE_AUTH_LOCATION", file.Name()); err != nil { 62 | log.Error(err, "error setting AZURE_AUTH_LOCATION environment variable") 63 | return err 64 | } 65 | 66 | authorizer, err := kvauth.NewAuthorizerFromFile(file.Name()) 67 | if err != nil { 68 | log.Error(err, "error creating authorizer") 69 | return err 70 | } 71 | 72 | client := keyvault.New() 73 | client.Authorizer = authorizer 74 | a.Client = client 75 | a.keyvault = akvCred.Keyvault 76 | 77 | return nil 78 | } 79 | 80 | // Get retrieves the secret associated with key from Azure Key Vault 81 | func (a *Backend) Get(key string, version string) (string, error) { 82 | 83 | if a.Client == nil { 84 | return "", errors.New("Azure Key Vault backend not initialized") 85 | } 86 | 87 | secretResp, err := a.Client.GetSecret(context.Background(), fmt.Sprintf("https://%s.vault.azure.net", a.keyvault), key, version) 88 | if err != nil { 89 | log.Error(err, "") 90 | return "", err 91 | } 92 | 93 | log.Info("Get secret succeeded") 94 | 95 | return *secretResp.Value, nil 96 | } 97 | 98 | // AzureCredentials represents expected credentials 99 | type AzureCredentials struct { 100 | TenantID string `json:"tenantId"` 101 | ClientID string `json:"clientId"` 102 | ClientSecret string `json:"clientSecret"` 103 | Keyvault string `json:"keyvault"` 104 | } 105 | -------------------------------------------------------------------------------- /pkg/akv/backend_test.go: -------------------------------------------------------------------------------- 1 | package akv 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" 8 | ) 9 | 10 | type mockedClient struct { 11 | } 12 | 13 | func TestNewBackend(t *testing.T) { 14 | backend := Backend{} 15 | _, err := backend.Get("hello", "") 16 | 17 | if err == nil { 18 | t.Errorf("There should have been an error because the backend has not been initialized") 19 | return 20 | } 21 | 22 | if err.Error() != "Azure Key Vault backend not initialized" { 23 | t.Error(err) 24 | return 25 | } 26 | } 27 | 28 | var flagtests = []struct { 29 | in string 30 | out string 31 | pass bool 32 | }{ 33 | {"hello", "hello", true}, 34 | {"hello", "world", false}, 35 | {"world", "world", true}, 36 | {"foo", "bar", false}, 37 | } 38 | 39 | func TestGetSecret(t *testing.T) { 40 | 41 | backend := Backend{} 42 | backend.Client = &mockedClient{} 43 | 44 | for _, tt := range flagtests { 45 | t.Run(tt.in, func(t *testing.T) { 46 | 47 | result, err := backend.Get(tt.in, "") 48 | 49 | if err != nil { 50 | t.Error(err) 51 | } else if (result == tt.out) != tt.pass { 52 | t.Errorf("Expected: %s, got: %s", tt.out, result) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func (m *mockedClient) GetSecret(context context.Context, url string, key string, version string) (keyvault.SecretBundle, error) { 59 | 60 | return keyvault.SecretBundle{Value: &key}, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/asm/backend.go: -------------------------------------------------------------------------------- 1 | // Package asm implements an external secret backend for AWS Secrets Manager. 2 | package asm 3 | 4 | import ( 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/secretsmanager" 11 | "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" 12 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 13 | "github.com/containersolutions/externalsecret-operator/pkg/utils" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | ) 16 | 17 | const ( 18 | defaultRegion = "eu-west-2" 19 | ) 20 | 21 | var ( 22 | log = ctrl.Log.WithName("asm") 23 | ) 24 | 25 | // Backend represents a backend for AWS Secrets Manager 26 | type Backend struct { 27 | SecretsManager secretsmanageriface.SecretsManagerAPI 28 | session *session.Session 29 | } 30 | 31 | func init() { 32 | backend.Register("asm", NewBackend) 33 | } 34 | 35 | // NewBackend returns an uninitialized Backend for AWS Secret Manager 36 | func NewBackend() backend.Backend { 37 | return &Backend{} 38 | } 39 | 40 | // Init initializes the Backend for AWS Secret Manager 41 | func (s *Backend) Init(parameters map[string]interface{}, credentials []byte) error { 42 | var err error 43 | 44 | s.session, err = utils.GetAWSSession(parameters, credentials, defaultRegion) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | s.SecretsManager = secretsmanager.New(s.session) 50 | return nil 51 | } 52 | 53 | // Get retrieves the secret associated with key from AWS Secrets Manager 54 | func (s *Backend) Get(key string, version string) (string, error) { 55 | _ = version 56 | 57 | input := &secretsmanager.GetSecretValueInput{ 58 | SecretId: aws.String(key), 59 | } 60 | err := input.Validate() 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | if s.SecretsManager == nil { 66 | log.Error(fmt.Errorf("error"), "backend not initialized") 67 | return "", fmt.Errorf("backend not initialized") 68 | } 69 | 70 | result, err := s.SecretsManager.GetSecretValue(input) 71 | if err != nil { 72 | log.Error(err, "Error getting secret value") 73 | return "", err 74 | } 75 | 76 | // https: //docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html 77 | // TLDR: Either SecretString or SecretBinary must have a value, but not both. They cannot both be empty. 78 | var secretValue string 79 | if result.SecretString != nil { 80 | secretValue = *result.SecretString 81 | } else { 82 | decodedBinarySecretBytes := make([]byte, base64.StdEncoding.DecodedLen(len(result.SecretBinary))) 83 | len, err := base64.StdEncoding.Decode(decodedBinarySecretBytes, result.SecretBinary) 84 | if err != nil { 85 | log.Error(err, "Base64 Decode Error:") 86 | return "", err 87 | } 88 | secretValue = string(decodedBinarySecretBytes[:len]) 89 | } 90 | return secretValue, nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/asm/backend_test.go: -------------------------------------------------------------------------------- 1 | package asm 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go/service/secretsmanager" 8 | "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | type mockedSecretsManager struct { 13 | secretsmanageriface.SecretsManagerAPI 14 | withError bool 15 | } 16 | 17 | func (m *mockedSecretsManager) GetSecretValue(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) { 18 | mockSecretString := *input.SecretId + "Value" 19 | mockedSecretBinary := []byte("b2ggbm8gVGhleSBjYW4gc2VlIHVzIG5vdw==") 20 | 21 | output := &secretsmanager.GetSecretValueOutput{ 22 | Name: input.SecretId, 23 | } 24 | 25 | if *input.SecretId == "secretKeyBinary" { 26 | output.SecretBinary = mockedSecretBinary 27 | } else { 28 | output.SecretString = &mockSecretString 29 | } 30 | if m.withError { 31 | return output, errors.New("oops") 32 | } 33 | return output, nil 34 | } 35 | 36 | func TestNewBackend(t *testing.T) { 37 | Convey("When creating a new ASM backend", t, func() { 38 | backend := NewBackend() 39 | So(backend, ShouldNotBeNil) 40 | So(backend, ShouldHaveSameTypeAs, &Backend{}) 41 | }) 42 | } 43 | 44 | func TestGet(t *testing.T) { 45 | secretKey := "secret" 46 | secretKeyBinary := "secretKeyBinary" 47 | keyVersion := "" 48 | secretValue := "secretValue" 49 | expectedValue := secretValue 50 | expectedSecretBinaryValue := "oh no They can see us now" 51 | 52 | Convey("Given an uninitialized AWSSecretsManagerBackend", t, func() { 53 | backend := Backend{} 54 | Convey("When retrieving a secret", func() { 55 | _, err := backend.Get(secretKey, keyVersion) 56 | Convey("Then an error is returned", func() { 57 | So(err, ShouldNotBeNil) 58 | So(err.Error(), ShouldEqual, "backend not initialized") 59 | }) 60 | }) 61 | }) 62 | 63 | Convey("Given an initialized AWSSecretsManagerBackend", t, func() { 64 | backend := Backend{} 65 | backend.SecretsManager = &mockedSecretsManager{} 66 | Convey("When retrieving a secret", func() { 67 | actualValue, err := backend.Get(secretKey, keyVersion) 68 | Convey("Then no error is returned", func() { 69 | So(err, ShouldBeNil) 70 | So(actualValue, ShouldEqual, expectedValue) 71 | }) 72 | }) 73 | }) 74 | 75 | Convey("Given an initialized AWSSecretsManagerBackend", t, func() { 76 | backend := Backend{} 77 | backend.SecretsManager = &mockedSecretsManager{} 78 | Convey("When retrieving a binary secret", func() { 79 | actualValue, err := backend.Get(secretKeyBinary, keyVersion) 80 | Convey("Then no error is returned", func() { 81 | So(err, ShouldBeNil) 82 | So(actualValue, ShouldEqual, expectedSecretBinaryValue) 83 | }) 84 | }) 85 | }) 86 | 87 | Convey("Given an initialized AWSSecretsManagerBackend (withError: true)", t, func() { 88 | backend := Backend{} 89 | backend.SecretsManager = &mockedSecretsManager{withError: true} 90 | Convey("When retrieving a secret", func() { 91 | _, err := backend.Get(secretKey, keyVersion) 92 | Convey("Then an error is returned", func() { 93 | So(err, ShouldNotBeNil) 94 | }) 95 | }) 96 | }) 97 | } 98 | 99 | type credentialsAndParametersTest struct { 100 | credentials string 101 | parameters map[string]interface{} 102 | expectedAccessKeyID string 103 | expectedRegion string 104 | expectedSecretAccessKey string 105 | expectedSessionToken string 106 | expectedErrorAssertion func(interface{}, ...interface{}) string 107 | expectedErrorString string 108 | } 109 | 110 | func TestInit(t *testing.T) { 111 | // https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ 112 | 113 | tests := []credentialsAndParametersTest{ 114 | { 115 | credentials: `{ 116 | "accessKeyID": "AKIABLABLA", 117 | "secretAccessKey": "SMMSsecrets", 118 | "sessionToken": "" 119 | }`, 120 | parameters: map[string]interface{}{ 121 | "region": "eu-mediterranean-1", 122 | }, 123 | expectedAccessKeyID: "AKIABLABLA", 124 | expectedRegion: "eu-mediterranean-1", 125 | expectedSecretAccessKey: "SMMSsecrets", 126 | expectedSessionToken: "", 127 | }, 128 | { 129 | credentials: `{ 130 | "accessKeyID": "AKIABLABLA", 131 | "secretAccessKey": "QW5vdGhlcmtleQoQW5vdGhlcmtleQo", 132 | "sessionToken": "" 133 | }`, 134 | parameters: map[string]interface{}{ 135 | "region": "eu-mediterranean-1", 136 | }, 137 | expectedAccessKeyID: "AKIABLABLA", 138 | expectedRegion: "eu-mediterranean-1", 139 | expectedSecretAccessKey: "QW5vdGhlcmtleQoQW5vdGhlcmtleQo", 140 | expectedSessionToken: "", 141 | }, 142 | { 143 | credentials: `{ 144 | "accessKeyID": "some", 145 | "secretAccessKey": "U29tZWtleQoU29tZWtleQo", 146 | "sessionToken": "" 147 | }`, 148 | parameters: map[string]interface{}{ 149 | "region": "other", 150 | }, 151 | expectedAccessKeyID: "some", 152 | expectedRegion: "other", 153 | expectedSecretAccessKey: "U29tZWtleQoU29tZWtleQo", 154 | expectedSessionToken: "", 155 | }, 156 | 157 | { 158 | credentials: `{ 159 | "accessKeyID": "some", 160 | "secretAccessKey": "VGhhdEtleQoVGhhdEtleQo", 161 | "sessionToken": "EtleQoVGhhEtleQoVGhh" 162 | }`, 163 | parameters: map[string]interface{}{ 164 | "region": "eu-west-2", 165 | }, 166 | expectedAccessKeyID: "some", 167 | expectedRegion: "eu-west-2", 168 | expectedSecretAccessKey: "VGhhdEtleQoVGhhdEtleQo", 169 | expectedSessionToken: "EtleQoVGhhEtleQoVGhh", 170 | }, 171 | 172 | { 173 | credentials: `{ 174 | "accessKeyID": "some", 175 | "secretAccessKey": "VGhhdEtleQoVGhhdEtleQo", 176 | "sessionToken": "tZWtletZWtle" 177 | }`, 178 | parameters: map[string]interface{}{ 179 | "region": "", 180 | }, 181 | expectedAccessKeyID: "some", 182 | expectedRegion: "eu-west-2", 183 | expectedSecretAccessKey: "VGhhdEtleQoVGhhdEtleQo", 184 | expectedSessionToken: "tZWtletZWtle", 185 | }, 186 | } 187 | 188 | for _, test := range tests { 189 | Convey("Given AWS credentials", t, func() { 190 | 191 | Convey("When initializing an ASM backend", func() { 192 | b := Backend{} 193 | err := b.Init(test.parameters, []byte(test.credentials)) 194 | So(err, ShouldBeNil) 195 | Convey("Then credentials are reflected in the AWS session", func() { 196 | actualCredentials, err := b.session.Config.Credentials.Get() 197 | So(err, ShouldBeNil) 198 | So(*b.session.Config.Region, ShouldEqual, test.expectedRegion) 199 | So(actualCredentials.AccessKeyID, ShouldEqual, test.expectedAccessKeyID) 200 | So(actualCredentials.SecretAccessKey, ShouldEqual, test.expectedSecretAccessKey) 201 | So(actualCredentials.SessionToken, ShouldEqual, test.expectedSessionToken) 202 | }) 203 | }) 204 | 205 | }) 206 | } 207 | 208 | Convey("When missing region parameter", t, func() { 209 | testParams := credentialsAndParametersTest{ 210 | credentials: `{ 211 | "accessKeyID": "AKIABLABLA", 212 | "secretAccessKey": "SMMSsecrets", 213 | "sessionToken": "" 214 | }`, 215 | parameters: map[string]interface{}{}, 216 | expectedAccessKeyID: "AKIABLABLA", 217 | expectedRegion: "eu-mediterranean-1", 218 | expectedSecretAccessKey: "SMMSsecrets", 219 | expectedSessionToken: "", 220 | } 221 | 222 | b := Backend{} 223 | err := b.Init(testParams.parameters, []byte(testParams.credentials)) 224 | Convey("Then an error is returned", func() { 225 | So(err, ShouldNotBeNil) 226 | So(err.Error(), ShouldEqual, "AWS region parameter missing") 227 | }) 228 | }) 229 | 230 | Convey("When invalid credentials are passed", t, func() { 231 | testParams := credentialsAndParametersTest{ 232 | credentials: "", 233 | parameters: map[string]interface{}{}, 234 | expectedAccessKeyID: "AKIABLABLA", 235 | expectedRegion: "eu-mediterranean-1", 236 | expectedSecretAccessKey: "SMMSsecrets", 237 | expectedSessionToken: "", 238 | } 239 | 240 | b := Backend{} 241 | err := b.Init(testParams.parameters, []byte(testParams.credentials)) 242 | Convey("Then an error is returned", func() { 243 | So(err, ShouldNotBeNil) 244 | So(err.Error(), ShouldEqual, "unexpected end of JSON input") 245 | }) 246 | }) 247 | } 248 | -------------------------------------------------------------------------------- /pkg/backend/backend.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | config "github.com/containersolutions/externalsecret-operator/pkg/config" 9 | ctrl "sigs.k8s.io/controller-runtime" 10 | ) 11 | 12 | var log = ctrl.Log.WithName("backend") 13 | 14 | // Backend is an abstract backend interface 15 | type Backend interface { 16 | Init(map[string]interface{}, []byte) error 17 | Get(string, string) (string, error) 18 | } 19 | 20 | // Instances are instantiated secret backends 21 | var Instances map[string]Backend 22 | 23 | // Functions is a map of labelled functions that return secret backend instances 24 | var Functions map[string]func() Backend 25 | 26 | var initLock sync.Mutex 27 | 28 | // Instantiate instantiates a Backend of type `backendType` 29 | func Instantiate(name string, backendType string) error { 30 | if Instances == nil { 31 | Instances = make(map[string]Backend) 32 | } 33 | 34 | function, found := Functions[backendType] 35 | if !found { 36 | log.Error(fmt.Errorf("error"), fmt.Sprintf("unknown backend type: '%v'", backendType)) 37 | return fmt.Errorf("unknown backend type: '%v'", backendType) 38 | } 39 | 40 | log.Info("Instantiate", "name", name, "type", backendType) 41 | Instances[name] = function() 42 | 43 | return nil 44 | } 45 | 46 | // Register registers a new backend type with name `name`staging 47 | // function is a function that returns a backend of that type 48 | func Register(name string, function func() Backend) { 49 | if Functions == nil { 50 | Functions = make(map[string]func() Backend) 51 | } 52 | 53 | log.Info("Register", "type", name) 54 | Functions[name] = function 55 | } 56 | 57 | // InitFromEnv initializes a backend looking into Env for config data 58 | func InitFromEnv(leaderID string) error { 59 | initLock.Lock() 60 | defer initLock.Unlock() 61 | log.Info("InitFromEnv", "availableBackends", strings.Join(availableBackends(), ",")) 62 | 63 | config, err := config.ConfigFromEnv() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | err = Instantiate(leaderID, config.Type) 69 | if err != nil { 70 | log.Error(err, "") 71 | return err 72 | } 73 | 74 | log.Info("Initialize", "name", leaderID) 75 | err = Instances[leaderID].Init(config.Parameters, []byte("")) 76 | 77 | return err 78 | } 79 | 80 | // InitFromCtrl initializes within a controller 81 | func InitFromCtrl(contrl string, config *config.Config, credentials []byte) error { 82 | initLock.Lock() 83 | defer initLock.Unlock() 84 | log.Info("InitFromCtrl", "availableBackends", strings.Join(availableBackends(), ",")) 85 | 86 | err := Instantiate(contrl, config.Type) 87 | if err != nil { 88 | log.Error(err, "") 89 | return err 90 | } 91 | 92 | log.Info("Initialize", "name", contrl) 93 | err = Instances[contrl].Init(config.Parameters, credentials) 94 | 95 | return err 96 | } 97 | 98 | func availableBackends() []string { 99 | backends := []string{} 100 | for k := range Functions { 101 | backends = append(backends, k) 102 | } 103 | return backends 104 | } 105 | -------------------------------------------------------------------------------- /pkg/backend/backend_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | config "github.com/containersolutions/externalsecret-operator/pkg/config" 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | type MockBackend struct { 14 | Param1 string 15 | } 16 | 17 | func NewBackend() Backend { 18 | return &MockBackend{} 19 | } 20 | 21 | func (m *MockBackend) Init(params map[string]interface{}, credentials []byte) error { 22 | m.Param1 = params["Param1"].(string) 23 | return nil 24 | } 25 | 26 | func (m *MockBackend) Get(key string, version string) (string, error) { 27 | return m.Param1, nil 28 | } 29 | 30 | func TestRegister(t *testing.T) { 31 | Convey("Given a mocked backend", t, func() { 32 | Convey("When registering it as a backend type", func() { 33 | Register("mock", NewBackend) 34 | Convey("Then the instantiation function is registered with the correct label", func() { 35 | function, found := Functions["mock"] 36 | So(found, ShouldBeTrue) 37 | So(function, ShouldEqual, NewBackend) 38 | }) 39 | }) 40 | }) 41 | } 42 | 43 | func TestInstantiate(t *testing.T) { 44 | Convey("Given a registered backend type", t, func() { 45 | Register("mock", NewBackend) 46 | Convey("When Instantiating it using the right label", func() { 47 | err := Instantiate("mock-backend", "mock") 48 | So(err, ShouldBeNil) 49 | Convey("Then a backend is instantiated with the right label", func() { 50 | backend, found := Instances["mock-backend"] 51 | So(found, ShouldBeTrue) 52 | So(reflect.TypeOf(backend), ShouldEqual, reflect.TypeOf(&MockBackend{})) 53 | }) 54 | }) 55 | Convey("When Instantiating it using the wrong label", func() { 56 | err := Instantiate("mock-backend", "mock-wrong-label") 57 | Convey("Then an error is returned", func() { 58 | So(err, ShouldNotBeNil) 59 | So(err.Error(), ShouldEqual, "unknown backend type: 'mock-wrong-label'") 60 | }) 61 | }) 62 | }) 63 | } 64 | 65 | func TestInitFromEnv(t *testing.T) { 66 | 67 | configStruct := config.Config{ 68 | Type: "mock", 69 | Parameters: map[string]interface{}{ 70 | "Param1": "Value1", 71 | }, 72 | } 73 | 74 | Convey("Given a registered backend type", t, func() { 75 | Register("mock", NewBackend) 76 | Convey("Given a valid config", func() { 77 | configData, _ := json.Marshal(configStruct) 78 | os.Setenv("OPERATOR_CONFIG", string(configData)) 79 | Convey("When initializing backend from env", func() { 80 | err := InitFromEnv("mock-backend") 81 | So(err, ShouldBeNil) 82 | Convey("Then a backend is instantiated and initialized correctly", func() { 83 | backend, found := Instances["mock-backend"] 84 | So(found, ShouldBeTrue) 85 | So(reflect.TypeOf(backend), ShouldEqual, reflect.TypeOf(&MockBackend{})) 86 | value, _ := backend.Get("", "") 87 | So(value, ShouldEqual, "Value1") 88 | }) 89 | }) 90 | }) 91 | 92 | Convey("Given a valid config with unknown backend type ", func() { 93 | configStruct.Type = "unknown" 94 | configData, _ := json.Marshal(configStruct) 95 | os.Setenv("OPERATOR_CONFIG", string(configData)) 96 | Convey("When initializing backend from env", func() { 97 | err := InitFromEnv("mock-backend") 98 | So(err, ShouldNotBeNil) 99 | Convey("Then an error message is returned", func() { 100 | So(err.Error(), ShouldEqual, "unknown backend type: 'unknown'") 101 | }) 102 | }) 103 | }) 104 | 105 | Convey("Given an invalid config", func() { 106 | os.Setenv("OPERATOR_CONFIG", "garbage") 107 | Convey("When initializing backend from env", func() { 108 | err := InitFromEnv("mock-backend") 109 | So(err, ShouldNotBeNil) 110 | Convey("Then an error is returned", func() { 111 | So(err.Error(), ShouldStartWith, "invalid") 112 | }) 113 | }) 114 | }) 115 | 116 | Convey("Given a missing config", func() { 117 | os.Unsetenv("OPERATOR_CONFIG") 118 | Convey("When initializing backend from env", func() { 119 | err := InitFromEnv("mock-backend") 120 | So(err, ShouldNotBeNil) 121 | Convey("Then an error is returned", func() { 122 | So(err.Error(), ShouldStartWith, "cannot find config") 123 | }) 124 | }) 125 | }) 126 | }) 127 | } 128 | 129 | func TestInitFromCtrl(t *testing.T) { 130 | var ( 131 | initConfig = config.Config{ 132 | Type: "mock", 133 | Parameters: map[string]interface{}{ 134 | "Param1": "Value1", 135 | }, 136 | Auth: map[string]interface{}{}, 137 | } 138 | 139 | credentials = `{ 140 | Credential: "dummy-creds" 141 | }` 142 | ) 143 | 144 | Convey("Given a registered backend type", t, func() { 145 | Register("mock", NewBackend) 146 | Convey("Given a valid config", func() { 147 | Convey("When initializing backend from contrl", func() { 148 | err := InitFromCtrl("test-ctrl", &initConfig, []byte(credentials)) 149 | So(err, ShouldBeNil) 150 | Convey("Then a backend is instantiated and initialized correctly", func() { 151 | backend, found := Instances["test-ctrl"] 152 | So(found, ShouldBeTrue) 153 | So(reflect.TypeOf(backend), ShouldEqual, reflect.TypeOf(&MockBackend{})) 154 | value, _ := backend.Get("", "") 155 | So(value, ShouldEqual, "Value1") 156 | }) 157 | }) 158 | }) 159 | 160 | Convey("Given a valid config with unknown backend type", func() { 161 | initConfig.Type = "unknown" 162 | 163 | Convey("When initializing backend from env", func() { 164 | err := InitFromCtrl("test-ctrl", &initConfig, []byte(credentials)) 165 | So(err, ShouldNotBeNil) 166 | Convey("Then an error message is returned", func() { 167 | So(err.Error(), ShouldEqual, "unknown backend type: 'unknown'") 168 | }) 169 | }) 170 | }) 171 | }) 172 | 173 | } 174 | -------------------------------------------------------------------------------- /pkg/backend/doc.go: -------------------------------------------------------------------------------- 1 | // Package backend implements the logic and data structures to handle external 2 | // backend backends. Each external secret implementation will reside in its own 3 | // package. A "Dummy" backend is provided as reference. 4 | // 5 | // Backends must register their "type" using a function to instantiate themselves. An 6 | // easy way of doing so is calling the secrets.Register function inside the 7 | // backend package init() function: 8 | // 9 | // func init() { 10 | // backend.Register("dummy", NewBackend) 11 | // } 12 | // 13 | // // NewBackend gives you an new Dummy Backend 14 | // func NewBackend() secrets.Backend { 15 | // return &Backend{} 16 | // } 17 | // 18 | package backend 19 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | ) 10 | 11 | var log = ctrl.Log.WithName("config") 12 | 13 | //ConfigEnvVar holds the name of the Environment Variable scanned for config 14 | const ConfigEnvVar string = "OPERATOR_CONFIG" 15 | 16 | //Config represent configuration information for the secrets backend 17 | type Config struct { 18 | Type string 19 | Parameters map[string]interface{} 20 | Auth map[string]interface{} 21 | } 22 | 23 | // ConfigFromJSON returns a Config object based on the string data passed as parameter 24 | func ConfigFromJSON(data string) (*Config, error) { 25 | backendConfig := &Config{} 26 | err := json.Unmarshal([]byte(data), backendConfig) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return backendConfig, nil 31 | } 32 | 33 | // ConfigFromCtrl returns a Config object based on the byte data passed as parameter 34 | func ConfigFromCtrl(data []byte) (*Config, error) { 35 | backendConfig := &Config{} 36 | err := json.Unmarshal(data, backendConfig) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return backendConfig, nil 41 | } 42 | 43 | //ConfigFromEnv parses Config from environment variable 44 | func ConfigFromEnv() (*Config, error) { 45 | data, present := os.LookupEnv(ConfigEnvVar) 46 | if !present { 47 | return nil, fmt.Errorf("cannot find config: `%v` not set", ConfigEnvVar) 48 | } 49 | 50 | if len(data) == 0 { 51 | return nil, fmt.Errorf("cannot find config: `%v` not set", ConfigEnvVar) 52 | } 53 | return ConfigFromJSON(data) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestBackendConfigFromJSON(t *testing.T) { 11 | Convey("Given a JSON backend config string", t, func() { 12 | configData := `{ 13 | "Type": "dummy", 14 | "Parameters": { 15 | "Suffix": "-ohlord" 16 | } 17 | }` 18 | 19 | Convey("When creating a Config object", func() { 20 | backendConfig, err := ConfigFromJSON(configData) 21 | So(err, ShouldBeNil) 22 | Convey("The data in Config is as expected", func() { 23 | So(backendConfig.Type, ShouldEqual, "dummy") 24 | So(backendConfig.Parameters, ShouldResemble, map[string]interface{}{"Suffix": "-ohlord"}) 25 | }) 26 | }) 27 | 28 | Convey("When creating a Config object from invalid JSON", func() { 29 | 30 | _, err := ConfigFromJSON("") 31 | So(err, ShouldNotBeNil) 32 | 33 | }) 34 | }) 35 | } 36 | 37 | func TestBackendConfigFromCtrl(t *testing.T) { 38 | Convey("Given a JSON RawMessage backend config string", t, func() { 39 | configData := `{ 40 | "type": "dummy", 41 | "auth": { 42 | "secretRef": { 43 | "name": "credential-secret", 44 | "namespace": "default" 45 | } 46 | }, 47 | "parameters": { 48 | "Suffix": "I am definitely a param" 49 | } 50 | }` 51 | 52 | Convey("When creating a Config object", func() { 53 | backendConfig, err := ConfigFromCtrl([]byte(configData)) 54 | So(err, ShouldBeNil) 55 | Convey("The data in Config is as expected", func() { 56 | So(backendConfig.Type, ShouldEqual, "dummy") 57 | So(backendConfig.Parameters, ShouldResemble, map[string]interface{}{"Suffix": "I am definitely a param"}) 58 | }) 59 | }) 60 | 61 | Convey("When creating a Config object from invalid JSON RawMessage", func() { 62 | 63 | _, err := ConfigFromCtrl([]byte{}) 64 | So(err, ShouldNotBeNil) 65 | 66 | }) 67 | }) 68 | } 69 | 70 | func TestConfigFromEnv(t *testing.T) { 71 | Convey("When backend config from env", t, func() { 72 | Convey("When creating a Config object from env", func() { 73 | var ( 74 | value = `{ 75 | "Type": "dummy", 76 | "Parameters": { 77 | "Suffix": "-ohlord" 78 | } 79 | }` 80 | key = "OPERATOR_CONFIG" 81 | ) 82 | 83 | os.Setenv(key, value) 84 | 85 | So(os.Getenv(key), ShouldNotBeBlank) 86 | 87 | backendConfig, err := ConfigFromEnv() 88 | So(err, ShouldBeNil) 89 | Convey("The data in Config is as expected", func() { 90 | So(backendConfig.Type, ShouldEqual, "dummy") 91 | So(backendConfig.Parameters, ShouldResemble, map[string]interface{}{"Suffix": "-ohlord"}) 92 | }) 93 | }) 94 | 95 | Convey("When creating a Config object from a blank env val", func() { 96 | var ( 97 | value = "" 98 | key = "OPERATOR_CONFIG" 99 | ) 100 | os.Setenv(key, value) 101 | 102 | So(os.Getenv(key), ShouldBeBlank) 103 | 104 | _, err := ConfigFromEnv() 105 | So(err, ShouldNotBeNil) 106 | So(err.Error(), ShouldEqual, "cannot find config: `OPERATOR_CONFIG` not set") 107 | }) 108 | 109 | Convey("When OPERATOR_CONFIG is not set", func() { 110 | key := "OPERATOR_CONFIG" 111 | 112 | os.Unsetenv(key) 113 | 114 | So(os.Getenv(key), ShouldBeBlank) 115 | 116 | _, err := ConfigFromEnv() 117 | So(err, ShouldNotBeNil) 118 | So(err.Error(), ShouldEqual, "cannot find config: `OPERATOR_CONFIG` not set") 119 | }) 120 | 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/credstash/backend.go: -------------------------------------------------------------------------------- 1 | // Package credstash implements backend for Credstash (that uses AWS KMS and DynamoDB) 2 | // Heavily inspired in github.com/ouzi-dev/credstash-operator using https://github.com/versent/unicreds 3 | package credstash 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 12 | "github.com/containersolutions/externalsecret-operator/pkg/utils" 13 | unicreds "github.com/versent/unicreds" 14 | 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | ) 17 | 18 | const ( 19 | defaultRegion = "eu-west-2" 20 | credstashVersionLength = 19 21 | ) 22 | 23 | var ( 24 | log = ctrl.Log.WithName("credstash") 25 | table = "" 26 | configEncryptionContext = make(map[string]string) 27 | ) 28 | 29 | // SecretManagerClientProvider will be our unicreds client 30 | type SecretManagerClientProvider interface { 31 | SetKMSConfig(config *aws.Config) 32 | SetDynamoDBConfig(config *aws.Config) 33 | GetHighestVersionSecret(tableName *string, name string, encContext *unicreds.EncryptionContextValue) (*unicreds.DecryptedCredential, error) 34 | GetSecret(tableName *string, name string, version string, encContext *unicreds.EncryptionContextValue) (*unicreds.DecryptedCredential, error) 35 | } 36 | 37 | // SecretManagerClient defining this struct to write methods for it 38 | type SecretManagerClient struct { 39 | } 40 | 41 | // SetKMSConfig sets configuration for KMS access 42 | func (s SecretManagerClient) SetKMSConfig(config *aws.Config) { 43 | unicreds.SetKMSConfig(config) 44 | } 45 | 46 | // SetDynamoDBConfig sets configuration for DynamoDB access 47 | func (s SecretManagerClient) SetDynamoDBConfig(config *aws.Config) { 48 | unicreds.SetDynamoDBConfig(config) 49 | } 50 | 51 | // GetHighestVersionSecret gets a secret with latest version from credstash 52 | func (s SecretManagerClient) GetHighestVersionSecret(tableName *string, name string, encContext *unicreds.EncryptionContextValue) (*unicreds.DecryptedCredential, error) { 53 | return unicreds.GetHighestVersionSecret(tableName, name, encContext) 54 | } 55 | 56 | // GetSecret gets a secret with specific version from credstash 57 | func (s SecretManagerClient) GetSecret(tableName *string, name string, version string, encContext *unicreds.EncryptionContextValue) (*unicreds.DecryptedCredential, error) { 58 | return unicreds.GetSecret(tableName, name, version, encContext) 59 | } 60 | 61 | // Backend represents a backend for Credstash 62 | type Backend struct { 63 | SecretsManager SecretManagerClientProvider 64 | session *session.Session 65 | } 66 | 67 | func init() { 68 | backend.Register("credstash", NewBackend) 69 | } 70 | 71 | // NewBackend returns an uninitialized Backend for Credstash 72 | func NewBackend() backend.Backend { 73 | return &Backend{} 74 | } 75 | 76 | // Init initializes the Backend for Credstash 77 | func (s *Backend) Init(parameters map[string]interface{}, credentials []byte) error { 78 | var err error 79 | s.SecretsManager = SecretManagerClient{} 80 | 81 | s.session, err = utils.GetAWSSession(parameters, credentials, defaultRegion) 82 | if err != nil { 83 | return err 84 | } 85 | var ok bool 86 | table, ok = parameters["table"].(string) 87 | if !ok { 88 | log.Error(nil, "Credstash Dynamo DB table key missing") 89 | return nil 90 | } 91 | 92 | configEncryptionContext, ok = parameters["encryptionContext"].(map[string]string) 93 | if !ok { 94 | log.Info("Not using security encryption context. Consider using it") 95 | } 96 | 97 | s.SecretsManager.SetKMSConfig(s.session.Config) 98 | s.SecretsManager.SetDynamoDBConfig(s.session.Config) 99 | return nil 100 | } 101 | 102 | // Get retrieves the secret associated with key from Credstash 103 | func (s *Backend) Get(key string, version string) (string, error) { 104 | if table == "" { 105 | table = "credential-store" 106 | } 107 | 108 | if s.SecretsManager == nil { 109 | log.Error(fmt.Errorf("error"), "backend not initialized") 110 | return "", fmt.Errorf("backend not initialized") 111 | } 112 | 113 | encryptionContext := unicreds.NewEncryptionContextValue() 114 | for k, v := range configEncryptionContext { 115 | if err := encryptionContext.Set(k + ":" + v); err != nil { 116 | return "", err 117 | } 118 | } 119 | if version == "" { 120 | creds, err := s.SecretsManager.GetHighestVersionSecret(aws.String(table), key, encryptionContext) 121 | if err != nil { 122 | log.Error(err, "Failed fetching secret from credstash", 123 | "Secret.Key", key, "Secret.Version", "latest", "Secret.Table", table, "Secret.Context", configEncryptionContext) 124 | 125 | return "", err 126 | } 127 | 128 | return creds.Secret, nil 129 | } 130 | formattedVersion, err := formatCredstashVersion(version) 131 | if err != nil { 132 | log.Error(err, "Failed formatting secret version", 133 | "Secret.Key", key, "Secret.Version", version, "Secret.Table", table, "Secret.Context", configEncryptionContext) 134 | return "", err 135 | } 136 | 137 | creds, err := s.SecretsManager.GetSecret(aws.String(table), key, formattedVersion, encryptionContext) 138 | if err != nil { 139 | log.Error(err, "Failed fetching secret from credstash", 140 | "Secret.Key", key, "Secret.Version", formattedVersion, "Secret.Table", table, "Secret.Context", configEncryptionContext) 141 | return "", err 142 | } 143 | 144 | return creds.Secret, nil 145 | } 146 | 147 | func formatCredstashVersion(inputVersion string) (string, error) { 148 | _, err := strconv.Atoi(inputVersion) 149 | if err != nil { 150 | log.Error(err, "Could not parse credstash version into number", 151 | "Secret.Version", inputVersion) 152 | return "", err 153 | } 154 | 155 | // we already have a padded version so nothing to do 156 | if len(inputVersion) == credstashVersionLength { 157 | return inputVersion, nil 158 | } 159 | 160 | // version is too longßß 161 | if len(inputVersion) > credstashVersionLength { 162 | return "", fmt.Errorf("version string is longer than supported. Maximum length is %d characters", 163 | credstashVersionLength) 164 | } 165 | 166 | // pad version with leading zeros until we reach credstashVersionLength 167 | // format becomes something like %019s which means pad the string until there's 19 0s 168 | format := fmt.Sprintf("%s%ds", "%0", credstashVersionLength) 169 | newVersion := fmt.Sprintf(format, inputVersion) 170 | 171 | return newVersion, nil 172 | } 173 | -------------------------------------------------------------------------------- /pkg/credstash/backend_test.go: -------------------------------------------------------------------------------- 1 | package credstash 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" 8 | . "github.com/smartystreets/goconvey/convey" 9 | unicreds "github.com/versent/unicreds" 10 | ) 11 | 12 | type mockedSecretsManager struct { 13 | secretsmanageriface.SecretsManagerAPI 14 | withError bool 15 | } 16 | 17 | // GetHighestVersionSecret mocked to return expected value 18 | func (s mockedSecretsManager) GetHighestVersionSecret(tableName *string, name string, encContext *unicreds.EncryptionContextValue) (*unicreds.DecryptedCredential, error) { 19 | var cred *unicreds.Credential 20 | return &unicreds.DecryptedCredential{Credential: cred, Secret: "secretValue"}, nil 21 | } 22 | 23 | // GetSecret mocked to return expected value 24 | func (s mockedSecretsManager) GetSecret(tableName *string, name string, version string, encContext *unicreds.EncryptionContextValue) (*unicreds.DecryptedCredential, error) { 25 | var cred *unicreds.Credential 26 | return &unicreds.DecryptedCredential{Credential: cred, Secret: "secretValue"}, nil 27 | } 28 | 29 | // SetKMSConfig sets configuration for KMS access 30 | func (s mockedSecretsManager) SetKMSConfig(config *aws.Config) { 31 | } 32 | 33 | // SetDynamoDBConfig sets configuration for DynamoDB access 34 | func (s mockedSecretsManager) SetDynamoDBConfig(config *aws.Config) { 35 | } 36 | 37 | func TestNewBackend(t *testing.T) { 38 | Convey("When creating a new Credstash backend", t, func() { 39 | backend := NewBackend() 40 | So(backend, ShouldNotBeNil) 41 | So(backend, ShouldHaveSameTypeAs, &Backend{}) 42 | }) 43 | } 44 | 45 | func TestGet(t *testing.T) { 46 | secretKey := "secret" 47 | keyVersion := "" 48 | secretValue := "secretValue" 49 | expectedValue := secretValue 50 | 51 | Convey("Given an uninitialized CredstashSecretsManagerBackend", t, func() { 52 | backend := Backend{} 53 | Convey("When retrieving a secret", func() { 54 | _, err := backend.Get(secretKey, keyVersion) 55 | Convey("Then an error is returned", func() { 56 | So(err, ShouldNotBeNil) 57 | So(err.Error(), ShouldEqual, "backend not initialized") 58 | }) 59 | }) 60 | }) 61 | 62 | Convey("Given an initialized AWSSecretsManagerBackend", t, func() { 63 | backend := Backend{} 64 | backend.SecretsManager = mockedSecretsManager{} 65 | Convey("When retrieving a secret", func() { 66 | actualValue, err := backend.Get(secretKey, keyVersion) 67 | Convey("Then no error is returned", func() { 68 | So(err, ShouldBeNil) 69 | So(actualValue, ShouldEqual, expectedValue) 70 | }) 71 | }) 72 | }) 73 | } 74 | 75 | type credentialsAndParametersTest struct { 76 | credentials string 77 | parameters map[string]interface{} 78 | expectedAccessKeyID string 79 | expectedRegion string 80 | expectedTable string 81 | expectedEncryptionContext map[string]string 82 | expectedSecretAccessKey string 83 | expectedSessionToken string 84 | expectedErrorAssertion func(interface{}, ...interface{}) string 85 | expectedErrorString string 86 | } 87 | 88 | func TestInit(t *testing.T) { 89 | 90 | tests := []credentialsAndParametersTest{ 91 | { 92 | credentials: `{ 93 | "accessKeyID": "AKIABLABLA", 94 | "secretAccessKey": "CredSsecrets", 95 | "sessionToken": "" 96 | }`, 97 | parameters: map[string]interface{}{ 98 | "region": "eu-mediterranean-1", 99 | "table": "credential-store", 100 | "encryptionContext": map[string]string{ 101 | "securityKey": "securityValue", 102 | }, 103 | }, 104 | expectedAccessKeyID: "AKIABLABLA", 105 | expectedRegion: "eu-mediterranean-1", 106 | expectedTable: "credential-store", 107 | expectedEncryptionContext: map[string]string{ 108 | "securityKey": "securityValue", 109 | }, 110 | expectedSecretAccessKey: "CredSsecrets", 111 | expectedSessionToken: "", 112 | }, 113 | { 114 | credentials: `{ 115 | "accessKeyID": "AKIABLABLA", 116 | "secretAccessKey": "QW5vdGhlcmtleQoQW5vdGhlcmtleQo", 117 | "sessionToken": "" 118 | }`, 119 | parameters: map[string]interface{}{ 120 | "region": "eu-mediterranean-1", 121 | "table": "credential-store", 122 | "encryptionContext": map[string]string{ 123 | "securityKey": "securityValue", 124 | }, 125 | }, 126 | expectedAccessKeyID: "AKIABLABLA", 127 | expectedRegion: "eu-mediterranean-1", 128 | expectedTable: "credential-store", 129 | expectedEncryptionContext: map[string]string{ 130 | "securityKey": "securityValue", 131 | }, 132 | expectedSecretAccessKey: "QW5vdGhlcmtleQoQW5vdGhlcmtleQo", 133 | expectedSessionToken: "", 134 | }, 135 | { 136 | credentials: `{ 137 | "accessKeyID": "some", 138 | "secretAccessKey": "U29tZWtleQoU29tZWtleQo", 139 | "sessionToken": "" 140 | }`, 141 | parameters: map[string]interface{}{ 142 | "region": "other", 143 | "table": "credential-store", 144 | "encryptionContext": map[string]string{ 145 | "securityKey": "securityValue", 146 | }, 147 | }, 148 | expectedAccessKeyID: "some", 149 | expectedRegion: "other", 150 | expectedTable: "credential-store", 151 | expectedEncryptionContext: map[string]string{ 152 | "securityKey": "securityValue", 153 | }, 154 | expectedSecretAccessKey: "U29tZWtleQoU29tZWtleQo", 155 | expectedSessionToken: "", 156 | }, 157 | 158 | { 159 | credentials: `{ 160 | "accessKeyID": "some", 161 | "secretAccessKey": "VGhhdEtleQoVGhhdEtleQo", 162 | "sessionToken": "EtleQoVGhhEtleQoVGhh" 163 | }`, 164 | parameters: map[string]interface{}{ 165 | "region": "eu-west-2", 166 | "table": "credential-store", 167 | "encryptionContext": map[string]string{ 168 | "securityKey": "securityValue", 169 | }, 170 | }, 171 | expectedAccessKeyID: "some", 172 | expectedRegion: "eu-west-2", 173 | expectedTable: "credential-store", 174 | expectedEncryptionContext: map[string]string{ 175 | "securityKey": "securityValue", 176 | }, 177 | expectedSecretAccessKey: "VGhhdEtleQoVGhhdEtleQo", 178 | expectedSessionToken: "EtleQoVGhhEtleQoVGhh", 179 | }, 180 | 181 | { 182 | credentials: `{ 183 | "accessKeyID": "some", 184 | "secretAccessKey": "VGhhdEtleQoVGhhdEtleQo", 185 | "sessionToken": "tZWtletZWtle" 186 | }`, 187 | parameters: map[string]interface{}{ 188 | "region": "", 189 | "table": "credential-store", 190 | "encryptionContext": map[string]string{ 191 | "securityKey": "securityValue", 192 | }, 193 | }, 194 | expectedAccessKeyID: "some", 195 | expectedRegion: "eu-west-2", 196 | expectedTable: "credential-store", 197 | expectedEncryptionContext: map[string]string{ 198 | "securityKey": "securityValue", 199 | }, 200 | expectedSecretAccessKey: "VGhhdEtleQoVGhhdEtleQo", 201 | expectedSessionToken: "tZWtletZWtle", 202 | }, 203 | } 204 | 205 | for _, test := range tests { 206 | Convey("Given AWS credentials", t, func() { 207 | 208 | Convey("When initializing an Credstash backend", func() { 209 | b := Backend{} 210 | err := b.Init(test.parameters, []byte(test.credentials)) 211 | So(err, ShouldBeNil) 212 | Convey("Then credentials are reflected in the AWS session", func() { 213 | actualCredentials, err := b.session.Config.Credentials.Get() 214 | So(err, ShouldBeNil) 215 | So(*b.session.Config.Region, ShouldEqual, test.expectedRegion) 216 | So(actualCredentials.AccessKeyID, ShouldEqual, test.expectedAccessKeyID) 217 | So(actualCredentials.SecretAccessKey, ShouldEqual, test.expectedSecretAccessKey) 218 | So(actualCredentials.SessionToken, ShouldEqual, test.expectedSessionToken) 219 | }) 220 | }) 221 | 222 | }) 223 | } 224 | 225 | Convey("When missing region parameter", t, func() { 226 | testParams := credentialsAndParametersTest{ 227 | credentials: `{ 228 | "accessKeyID": "AKIABLABLA", 229 | "secretAccessKey": "CredSsecrets", 230 | "sessionToken": "" 231 | }`, 232 | parameters: map[string]interface{}{}, 233 | expectedAccessKeyID: "AKIABLABLA", 234 | expectedRegion: "eu-mediterranean-1", 235 | expectedSecretAccessKey: "CredSsecrets", 236 | expectedSessionToken: "", 237 | } 238 | 239 | b := Backend{} 240 | err := b.Init(testParams.parameters, []byte(testParams.credentials)) 241 | Convey("Then an error is returned", func() { 242 | So(err, ShouldNotBeNil) 243 | So(err.Error(), ShouldEqual, "AWS region parameter missing") 244 | }) 245 | }) 246 | 247 | Convey("When invalid credentials are passed", t, func() { 248 | testParams := credentialsAndParametersTest{ 249 | credentials: "", 250 | parameters: map[string]interface{}{}, 251 | expectedAccessKeyID: "AKIABLABLA", 252 | expectedRegion: "eu-mediterranean-1", 253 | expectedSecretAccessKey: "CredSsecrets", 254 | expectedSessionToken: "", 255 | } 256 | 257 | b := Backend{} 258 | err := b.Init(testParams.parameters, []byte(testParams.credentials)) 259 | Convey("Then an error is returned", func() { 260 | So(err, ShouldNotBeNil) 261 | So(err.Error(), ShouldEqual, "unexpected end of JSON input") 262 | }) 263 | }) 264 | } 265 | -------------------------------------------------------------------------------- /pkg/dummy/backend.go: -------------------------------------------------------------------------------- 1 | // Package dummy implements an example backend that can be used for testing 2 | // purposes. It acceps a "suffix" as a parameter that will be appended to the 3 | // key passed to the Get function. 4 | package dummy 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 10 | ctrl "sigs.k8s.io/controller-runtime" 11 | ) 12 | 13 | var log = ctrl.Log.WithName("dummy") 14 | 15 | // Backend is a fake secrets backend for testing purposes 16 | type Backend struct { 17 | suffix string 18 | } 19 | 20 | func init() { 21 | backend.Register("dummy", NewBackend) 22 | } 23 | 24 | // NewBackend gives you an NewBackend Dummy Backend 25 | func NewBackend() backend.Backend { 26 | return &Backend{} 27 | } 28 | 29 | // Init implements SecretsBackend interface, sets the suffix 30 | func (d *Backend) Init(parameters map[string]interface{}, credentials []byte) error { 31 | if len(parameters) == 0 { 32 | log.Error(fmt.Errorf("error"), "empty or invalid parameters: ") 33 | return fmt.Errorf("empty or invalid parameters") 34 | } 35 | 36 | suffix, ok := parameters["Suffix"].(string) 37 | if !ok { 38 | log.Error(fmt.Errorf("error"), "missing parameters: ") 39 | return fmt.Errorf("missing parameters") 40 | } 41 | 42 | d.suffix = suffix 43 | return nil 44 | } 45 | 46 | // Get a key and returns a fake secrets key + suffix 47 | func (d *Backend) Get(key string, version string) (string, error) { 48 | if d.suffix == "" { 49 | return "", fmt.Errorf("backend is not initialized") 50 | } 51 | 52 | if key == "" { 53 | return "", fmt.Errorf("empty key provided") 54 | } 55 | 56 | if key == "ErroredKey" { 57 | return "", fmt.Errorf("Mocked error") 58 | } 59 | 60 | return key + version + d.suffix, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/dummy/backend_test.go: -------------------------------------------------------------------------------- 1 | package dummy 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestNewBackend(t *testing.T) { 10 | Convey("When creating a new dummy backend", t, func() { 11 | backend := NewBackend() 12 | So(backend, ShouldNotBeNil) 13 | So(backend, ShouldHaveSameTypeAs, &Backend{}) 14 | }) 15 | } 16 | 17 | func TestGet(t *testing.T) { 18 | var ( 19 | secretKey = "secret" 20 | keyVersion = "latest" 21 | testSuffix = "test-suffix" 22 | expectedValue = secretKey + keyVersion + testSuffix 23 | ) 24 | 25 | Convey("Given an uninitialized dummy backend", t, func() { 26 | backend := Backend{} 27 | Convey("When retrieving a secret", func() { 28 | _, err := backend.Get(secretKey, keyVersion) 29 | Convey("Then an error is returned", func() { 30 | So(err, ShouldNotBeNil) 31 | So(err.Error(), ShouldEqual, "backend is not initialized") 32 | }) 33 | }) 34 | }) 35 | 36 | Convey("Given an initialized dummy backend", t, func() { 37 | backend := Backend{} 38 | backend.suffix = testSuffix 39 | Convey("When retrieving a secret", func() { 40 | actualValue, err := backend.Get(secretKey, keyVersion) 41 | Convey("Then no error is returned", func() { 42 | So(err, ShouldBeNil) 43 | So(actualValue, ShouldEqual, expectedValue) 44 | }) 45 | }) 46 | 47 | Convey("When retrieving secret details", func() { 48 | _, err := backend.Get("", "") 49 | Convey("An error is returned when key is empty", func() { 50 | So(err, ShouldNotBeNil) 51 | So(err.Error(), ShouldEqual, "empty key provided") 52 | 53 | }) 54 | }) 55 | 56 | Convey("When mock error key is provided", func() { 57 | _, err := backend.Get("ErroredKey", "") 58 | Convey("An error is returned when key is a mock error key", func() { 59 | So(err, ShouldNotBeNil) 60 | So(err.Error(), ShouldEqual, "Mocked error") 61 | 62 | }) 63 | }) 64 | }) 65 | } 66 | 67 | func TestInit(t *testing.T) { 68 | var ( 69 | params = make(map[string]interface{}) 70 | credentials = []byte{} 71 | ) 72 | 73 | params["Suffix"] = "dummy init" 74 | Convey("Should initialize backend", t, func() { 75 | backend := Backend{} 76 | credentials = []byte{} 77 | err := backend.Init(params, credentials) 78 | So(err, ShouldBeNil) 79 | }) 80 | 81 | Convey("Should fail initialize backend with invalid config", t, func() { 82 | backend := Backend{} 83 | err := backend.Init(make(map[string]interface{}), credentials) 84 | So(err, ShouldNotBeNil) 85 | So(err.Error(), ShouldEqual, "empty or invalid parameters") 86 | }) 87 | 88 | Convey("Should fail initialize backend with invalid parameter suffix", t, func() { 89 | backend := Backend{} 90 | err := backend.Init(map[string]interface{}{"unknown": "fail"}, credentials) 91 | So(err, ShouldNotBeNil) 92 | So(err.Error(), ShouldEqual, "missing parameters") 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /pkg/gitlab/backend.go: -------------------------------------------------------------------------------- 1 | // Package gitlab implements a gitlab backend that can be used to inject 2 | // Gitlab CI/CD variables as kubernetes secrets. 3 | package gitlab 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | 9 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 10 | gitlab "github.com/xanzy/go-gitlab" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | ) 13 | 14 | var log = ctrl.Log.WithName("gitlab") 15 | 16 | type ErrInitFailed struct { 17 | message string 18 | } 19 | 20 | func (e *ErrInitFailed) Error() string { 21 | return fmt.Sprintf("gitlab backend init failed: %s", e.message) 22 | } 23 | 24 | type ErrGet struct { 25 | itemName string 26 | message string 27 | } 28 | 29 | func (e *ErrGet) Error() string { 30 | return fmt.Sprintf("gitlab backend get '%s' failed: %s", e.itemName, e.message) 31 | } 32 | 33 | // Backend is a gitlab variables backend 34 | type Backend struct { 35 | client *gitlab.Client 36 | projectID interface{} 37 | } 38 | 39 | func init() { 40 | backend.Register("gitlab", NewBackend) 41 | } 42 | 43 | // NewBackend gives you a new gitlab backend 44 | func NewBackend() backend.Backend { 45 | return &Backend{} 46 | } 47 | 48 | // Init implements SecretsBackend interface 49 | func (d *Backend) Init(parameters map[string]interface{}, credentials []byte) error { 50 | var err error 51 | 52 | if len(parameters) == 0 { 53 | log.Error(fmt.Errorf("error"), "empty or invalid parameters: ") 54 | return fmt.Errorf("empty or invalid parameters") 55 | } 56 | 57 | gitlabCreds := &GitlabCredentials{} 58 | if err := json.Unmarshal(credentials, gitlabCreds); err != nil { 59 | log.Error(err, "Unmarshalling failed") 60 | return &ErrInitFailed{message: err.Error()} 61 | } 62 | 63 | baseURL, ok := parameters["baseURL"] 64 | if !ok { 65 | log.Error(fmt.Errorf("error"), "missing baseURL parameter: ") 66 | return fmt.Errorf("missing baseURL parameter") 67 | } 68 | 69 | projectID, ok := parameters["projectID"] 70 | if !ok { 71 | log.Error(fmt.Errorf("error"), "missing projectID parameter: ") 72 | return fmt.Errorf("missing projectID parameter") 73 | } 74 | 75 | d.projectID = projectID 76 | d.client, err = gitlab.NewClient(gitlabCreds.Token, gitlab.WithBaseURL(baseURL.(string))) 77 | if err != nil { 78 | log.Error(fmt.Errorf("error"), "failed to create client: ") 79 | return fmt.Errorf("failed to create client") 80 | } 81 | return nil 82 | } 83 | 84 | // Get takes a key and version, and returns the value 85 | func (d *Backend) Get(key string, version string) (string, error) { 86 | if key == "" { 87 | return "", fmt.Errorf("empty key provided") 88 | } 89 | 90 | variable, _, err := d.client.ProjectVariables.GetVariable(fmt.Sprintf("%.f", d.projectID), key, nil) 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | log.Info("Get was successful for the Gitlab") 96 | 97 | return variable.Value, nil 98 | } 99 | 100 | type GitlabCredentials struct { 101 | Token string `json:"token"` 102 | } 103 | -------------------------------------------------------------------------------- /pkg/gsm/backend.go: -------------------------------------------------------------------------------- 1 | // Package gsm implements backend for Google Secrets Manager 2 | package gsm 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | 8 | "cloud.google.com/go/iam" 9 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 10 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 11 | "github.com/googleapis/gax-go" 12 | "golang.org/x/oauth2/google" 13 | option "google.golang.org/api/option" 14 | "google.golang.org/grpc" 15 | 16 | secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" 17 | iampb "google.golang.org/genproto/googleapis/iam/v1" 18 | ctrl "sigs.k8s.io/controller-runtime" 19 | // iampb "google.golang.org/genproto/googleapis/iam/v1" 20 | ) 21 | 22 | const ( 23 | cloudPlatformRole = "https://www.googleapis.com/auth/cloud-platform" 24 | defaultVersion = "latest" 25 | ) 26 | 27 | var log = ctrl.Log.WithName("gsm") 28 | 29 | type GoogleSecretManagerClient interface { 30 | AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) 31 | AddSecretVersion(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) 32 | Connection() *grpc.ClientConn 33 | CreateSecret(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) 34 | DeleteSecret(ctx context.Context, req *secretmanagerpb.DeleteSecretRequest, opts ...gax.CallOption) error 35 | DestroySecretVersion(ctx context.Context, req *secretmanagerpb.DestroySecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) 36 | DisableSecretVersion(ctx context.Context, req *secretmanagerpb.DisableSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) 37 | EnableSecretVersion(ctx context.Context, req *secretmanagerpb.EnableSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) 38 | GetIamPolicy(ctx context.Context, req *iampb.GetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) 39 | GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) 40 | GetSecretVersion(ctx context.Context, req *secretmanagerpb.GetSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) 41 | IAM(name string) *iam.Handle 42 | ListSecretVersions(ctx context.Context, req *secretmanagerpb.ListSecretVersionsRequest, opts ...gax.CallOption) *secretmanager.SecretVersionIterator 43 | ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator 44 | SetIamPolicy(ctx context.Context, req *iampb.SetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) 45 | TestIamPermissions(ctx context.Context, req *iampb.TestIamPermissionsRequest, opts ...gax.CallOption) (*iampb.TestIamPermissionsResponse, error) 46 | UpdateSecret(ctx context.Context, req *secretmanagerpb.UpdateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) 47 | Close() error 48 | } 49 | 50 | // Backend for Google Secrets Manager 51 | type Backend struct { 52 | projectID string 53 | SecretManagerClient GoogleSecretManagerClient 54 | } 55 | 56 | func init() { 57 | backend.Register("gsm", NewBackend) 58 | } 59 | 60 | // NewBackend gives you an empty Google Secrets Manager Backend 61 | func NewBackend() backend.Backend { 62 | return &Backend{} 63 | } 64 | 65 | // Init initializes Google secretmanager backend 66 | func (g *Backend) Init(parameters map[string]interface{}, credentials []byte) error { 67 | ctx := context.Background() 68 | 69 | if len(parameters) == 0 || len(credentials) == 0 { 70 | return fmt.Errorf("credentials or parameters invalid") 71 | } 72 | 73 | projectID, ok := parameters["projectID"].(string) 74 | if !ok { 75 | return fmt.Errorf("parameters invalid") 76 | } 77 | 78 | g.projectID = projectID 79 | 80 | config, err := google.JWTConfigFromJSON(credentials, cloudPlatformRole) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | ts := config.TokenSource(ctx) 86 | 87 | client, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts)) 88 | if err != nil { 89 | return fmt.Errorf("failed to create secretmanager client: %v", err) 90 | } 91 | 92 | g.SecretManagerClient = client 93 | 94 | return nil 95 | } 96 | 97 | // Get retrieves key from Google SecretManager 98 | func (g *Backend) Get(key string, version string) (string, error) { 99 | ctx := context.Background() 100 | 101 | if g.SecretManagerClient == nil || g.projectID == "" { 102 | log.Error(fmt.Errorf("error"), "backend is not initialized") 103 | return "", fmt.Errorf("backend is not initialized") 104 | } 105 | 106 | if version == "" { 107 | version = defaultVersion 108 | } 109 | 110 | name := fmt.Sprintf("projects/%s/secrets/%s/versions/%s", g.projectID, key, version) 111 | 112 | req := &secretmanagerpb.AccessSecretVersionRequest{ 113 | Name: name, 114 | } 115 | 116 | result, err := g.SecretManagerClient.AccessSecretVersion(ctx, req) 117 | if err != nil { 118 | return "", fmt.Errorf("failed to access secret version: %v", err) 119 | } 120 | 121 | return string(result.Payload.Data), nil 122 | } 123 | -------------------------------------------------------------------------------- /pkg/internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func AssertEquals(t *testing.T, expected interface{}, actual interface{}) { 9 | if actual != expected { 10 | t.Fail() 11 | fmt.Printf("expected '%s' got %s'", expected, actual) 12 | } 13 | } 14 | 15 | func AssertNotNil(t *testing.T, value interface{}) { 16 | if value == nil { 17 | t.Fail() 18 | fmt.Printf("expected '%s' not to be nil", value) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/register/register.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | // Register your backends here 5 | _ "github.com/containersolutions/externalsecret-operator/pkg/akv" 6 | _ "github.com/containersolutions/externalsecret-operator/pkg/asm" 7 | _ "github.com/containersolutions/externalsecret-operator/pkg/credstash" 8 | _ "github.com/containersolutions/externalsecret-operator/pkg/dummy" 9 | _ "github.com/containersolutions/externalsecret-operator/pkg/gitlab" 10 | _ "github.com/containersolutions/externalsecret-operator/pkg/gsm" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/register/register_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/containersolutions/externalsecret-operator/pkg/backend" 7 | ) 8 | 9 | var expectedRegisteredBackends = []string{ 10 | "asm", 11 | "dummy", 12 | "gitlab", 13 | "gsm", 14 | } 15 | 16 | func TestInit(t *testing.T) { 17 | for _, k := range expectedRegisteredBackends { 18 | _, found := backend.Functions[k] 19 | if !found { 20 | t.Errorf("registered backend expected but not found: '%v'", k) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/json" 6 | "fmt" 7 | "math/big" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/credentials" 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | ) 14 | 15 | const validObjChars = "0123456789abcdefghijklmnopqrstuvwxyz" 16 | 17 | var ( 18 | log = ctrl.Log.WithName("asm") 19 | ) 20 | 21 | // RandomBytes generate random bytes 22 | func RandomBytes(n int) ([]byte, error) { 23 | b := make([]byte, n) 24 | _, err := rand.Read(b) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return b, nil 30 | } 31 | 32 | // RandomInt returns a random int64 33 | func RandomInt() (int64, error) { 34 | randomInt, err := rand.Int(rand.Reader, big.NewInt(int64(len(validObjChars)))) 35 | if err != nil { 36 | return 0, err 37 | } 38 | 39 | return randomInt.Int64(), nil 40 | } 41 | 42 | // RandomStringObjectSafe returns a random string that is safe to use as an k8s object Name 43 | // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ 44 | func RandomStringObjectSafe(n int) (string, error) { 45 | b, err := RandomBytes(n) 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | for i := range b { 51 | randomInt, err := RandomInt() 52 | if err != nil { 53 | return "", err 54 | } 55 | b[i] = validObjChars[randomInt] 56 | } 57 | return string(b), nil 58 | 59 | } 60 | 61 | // AWSCredentials represents expected credentials 62 | type AWSCredentials struct { 63 | AccessKeyID string 64 | SecretAccessKey string 65 | SessionToken string 66 | } 67 | 68 | /* GetAWSSession returns an aws.session.Session based on the parameters or environment variables 69 | * If parameters are not present or incomplete (secret key, access key AND region) 70 | * then let default config loading order to go on: 71 | * https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ 72 | */ 73 | func GetAWSSession(parameters map[string]interface{}, creds []byte, defaultRegion string) (*session.Session, error) { 74 | awsCreds := &AWSCredentials{} 75 | if err := json.Unmarshal(creds, awsCreds); err != nil { 76 | log.Error(err, "Unmarshalling failed") 77 | return nil, err 78 | } 79 | 80 | region, ok := parameters["region"].(string) 81 | if !ok { 82 | log.Error(nil, "AWS region parameter missing") 83 | return nil, fmt.Errorf("AWS region parameter missing") 84 | } 85 | 86 | if region == "" { 87 | region = defaultRegion 88 | } 89 | 90 | return session.NewSession(&aws.Config{ 91 | Region: aws.String(region), 92 | Credentials: credentials.NewStaticCredentials( 93 | awsCreds.AccessKeyID, 94 | awsCreds.SecretAccessKey, 95 | awsCreds.SessionToken), 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /pkg/utils/utils_suite_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestUtils(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Utils Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | utils "github.com/containersolutions/externalsecret-operator/pkg/utils" 8 | ) 9 | 10 | var _ = Describe("Utils", func() { 11 | 12 | BeforeEach(func() {}) 13 | 14 | AfterEach(func() {}) 15 | Context("Should generate random base64 encoded string", func() { 16 | It("Should succeed", func() { 17 | _, err := utils.RandomStringObjectSafe(40) 18 | Expect(err).To(BeNil()) 19 | }) 20 | 21 | It("Should not be nil", func() { 22 | str, err := utils.RandomStringObjectSafe(40) 23 | Expect(err).To(BeNil()) 24 | Expect(str).ToNot(BeNil()) 25 | }) 26 | }) 27 | 28 | Context("Should generate random bytes", func() { 29 | It("Should succeed", func() { 30 | _, err := utils.RandomBytes(40) 31 | Expect(err).To(BeNil()) 32 | }) 33 | 34 | It("Should not be nil", func() { 35 | str, err := utils.RandomBytes(40) 36 | Expect(err).To(BeNil()) 37 | Expect(str).ToNot(BeNil()) 38 | }) 39 | }) 40 | 41 | Context("Should generate int64", func() { 42 | It("Should succeed", func() { 43 | _, err := utils.RandomInt() 44 | Expect(err).To(BeNil()) 45 | }) 46 | 47 | It("Should not be nil", func() { 48 | ranInt64, err := utils.RandomInt() 49 | Expect(err).To(BeNil()) 50 | Expect(ranInt64).ToNot(BeNil()) 51 | }) 52 | }) 53 | 54 | }) 55 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | //Version represents the version of the operator, used in build 5 | Version = "0.1.0" 6 | ) 7 | --------------------------------------------------------------------------------