├── .VERSION ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── build.yml │ ├── pr-check-signed-commits.yml │ ├── release-pr.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── USAGEGUIDE.md ├── api └── v1 │ ├── groupversion_info.go │ ├── onepassworditem_types.go │ └── zz_generated.deepcopy.go ├── cmd └── main.go ├── config ├── connect │ ├── deployment.yaml │ └── service.yaml ├── crd │ ├── bases │ │ └── onepassword.com_onepassworditems.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_onepassworditems.yaml │ │ └── webhook_in_onepassworditems.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── manifests │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── onepassworditem_editor_role.yaml │ ├── onepassworditem_viewer_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── samples │ ├── kustomization.yaml │ └── onepassword_v1_onepassworditem.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal └── controller │ ├── deployment_controller.go │ ├── deployment_controller_test.go │ ├── onepassworditem_controller.go │ ├── onepassworditem_controller_test.go │ └── suite_test.go ├── pkg ├── kubernetessecrets │ ├── kubernetes_secrets_builder.go │ └── kubernetes_secrets_builder_test.go ├── logs │ └── log_levels.go ├── mocks │ └── mocksecretserver.go ├── onepassword │ ├── annotations.go │ ├── annotations_test.go │ ├── connect_setup.go │ ├── connect_setup_test.go │ ├── containers.go │ ├── containers_test.go │ ├── deployments.go │ ├── deployments_test.go │ ├── items.go │ ├── object_generators_for_test.go │ ├── secret_update_handler.go │ ├── secret_update_handler_test.go │ ├── uuid.go │ ├── volumes.go │ └── volumes_test.go └── utils │ ├── k8sutil.go │ └── string.go ├── scripts └── prepare-release.sh └── version └── version.go /.VERSION: -------------------------------------------------------------------------------- 1 | 1.8.1 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report bugs and errors found while using the Operator. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Your environment 11 | 12 | 13 | Operator Version: 14 | 15 | 19 | Connect Server Version: 20 | 21 | 22 | Kubernetes Version: 23 | 24 | ## What happened? 25 | 26 | 27 | ## What did you expect to happen? 28 | 29 | 30 | ## Steps to reproduce 31 | 1. 32 | 33 | 34 | ## Notes & Logs 35 | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # docs: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 2 | blank_issues_enabled: true 3 | contact_links: 4 | - name: 1Password Community 5 | url: https://1password.community/categories/secrets-automation 6 | about: Please ask general Secrets Automation questions here. 7 | - name: 1Password Security Bug Bounty 8 | url: https://bugcrowd.com/agilebits 9 | about: Please report security vulnerabilities here. 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for the Operator 4 | title: '' 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Summary 11 | 12 | 13 | ### Use cases 14 | 17 | 18 | ### Proposed solution 19 | 22 | 23 | ### Is there a workaround to accomplish this today? 24 | 26 | 27 | ### References & Prior Work 28 | 30 | 31 | * 32 | * 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Set up Go 1.x 10 | uses: actions/setup-go@v4 11 | with: 12 | go-version: ^1.21 13 | 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v4 16 | 17 | - name: Build 18 | run: go build -v ./... 19 | 20 | - name: Test 21 | run: make test 22 | -------------------------------------------------------------------------------- /.github/workflows/pr-check-signed-commits.yml: -------------------------------------------------------------------------------- 1 | name: Check signed commits in PR 2 | on: pull_request_target 3 | 4 | jobs: 5 | build: 6 | name: Check signed commits in PR 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check signed commits in PR 13 | uses: 1Password/check-signed-commits-action@v1 14 | -------------------------------------------------------------------------------- /.github/workflows/release-pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | create: 3 | branches: 4 | 5 | name: Open Release PR for review 6 | 7 | jobs: 8 | # This job is necessary because GitHub does not (yet) support 9 | # filtering `create` triggers by branch name. 10 | # See: https://github.community/t/trigger-job-on-branch-created/16878/5 11 | should_create_pr: 12 | name: Check if PR for branch already exists 13 | runs-on: ubuntu-latest 14 | outputs: 15 | result: ${{ steps.is_release_branch_without_pr.outputs.result }} 16 | steps: 17 | - 18 | id: is_release_branch_without_pr 19 | name: Find matching PR 20 | uses: actions/github-script@v7 21 | with: 22 | github-token: ${{ secrets.GITHUB_TOKEN }} 23 | script: | 24 | // Search for an existing PR with head & base 25 | // that match the created branch 26 | 27 | const [releaseBranchName] = context.ref.match("release/v[0-9]+\.[0-9]+\.[0-9]+") || [] 28 | 29 | if(!releaseBranchName) { return false } 30 | 31 | const {data: prs} = await github.rest.pulls.list({ 32 | ...context.repo, 33 | state: 'open', 34 | head: `1Password:${releaseBranchName}`, 35 | base: context.payload.master_branch 36 | }) 37 | 38 | return prs.length === 0 39 | 40 | create_pr: 41 | needs: should_create_pr 42 | if: needs.should_create_pr.outputs.result == 'true' 43 | name: Create Release Pull Request 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Parse release version 49 | id: get_version 50 | run: echo "version=$(echo "$GITHUB_REF" | sed 's|^refs/heads/release/v?*||g')" >> $GITHUB_OUTPUT 51 | 52 | - name: Prepare Pull Request 53 | id: prep_pr 54 | run: | 55 | CHANGELOG_PATH=$(printf "%s/CHANGELOG.md" "${GITHUB_WORKSPACE}") 56 | LOG_ENTRY=$(awk '/START\/v[0-9]+\.[0-9]+\.[0-9]+*/{f=1; next} /---/{if (f == 1) exit} f' "${CHANGELOG_PATH}") 57 | DELIMITER="$(openssl rand -hex 8)" # DELIMITER is randomly generated and unique for each run. For more information, see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections. 58 | 59 | PR_BODY_CONTENT=" 60 | This is an automated PR for a new release. 61 | 62 | Please check the following before approving: 63 | - [ ] Changelog is accurate. The documented changes for this release are printed below. 64 | - [ ] Any files referencing a version number. Confirm it matches the version number in the branch name. 65 | --- 66 | ## Release Changelog Preview 67 | ${LOG_ENTRY} 68 | " 69 | 70 | echo "pr_body<<${DELIMITER}${PR_BODY_CONTENT}${DELIMITER}" >> "${GITHUB_OUTPUT}" 71 | 72 | - name: Create Pull Request via API 73 | id: post_pr 74 | uses: octokit/request-action@v2.x 75 | with: 76 | route: POST /repos/${{ github.repository }}/pulls 77 | title: ${{ format('Prepare Release - v{0}', steps.get_version.outputs.version) }} 78 | head: ${{ github.ref }} 79 | base: ${{ github.event.master_branch }} 80 | body: ${{ toJson(steps.prep_pr.outputs.pr_body) }} 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release-docker: 10 | runs-on: ubuntu-latest 11 | env: 12 | DOCKER_CLI_EXPERIMENTAL: "enabled" 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Docker meta 20 | id: meta 21 | uses: docker/metadata-action@v5 22 | with: 23 | images: | 24 | 1password/onepassword-operator 25 | # Publish image for x.y.z and x.y 26 | # The latest tag is automatically added for semver tags 27 | tags: | 28 | type=semver,pattern={{version}} 29 | type=semver,pattern={{major}}.{{minor}} 30 | 31 | - name: Get the version from tag 32 | id: get_version 33 | run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} 34 | 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3 37 | 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v3 40 | 41 | - name: Docker Login 42 | uses: docker/login-action@v3 43 | with: 44 | username: ${{ secrets.DOCKERHUB_USERNAME }} 45 | password: ${{ secrets.DOCKERHUB_TOKEN }} 46 | 47 | - name: Build and push 48 | uses: docker/build-push-action@v5 49 | with: 50 | context: . 51 | file: Dockerfile 52 | platforms: linux/amd64,linux/arm64,linux/arm/v7 53 | push: true 54 | tags: ${{ steps.meta.outputs.tags }} 55 | labels: ${{ steps.meta.outputs.labels }} 56 | build-args: | 57 | operator_version=${{ steps.get_version.outputs.VERSION }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *~ 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [//]: # (START/LATEST) 2 | # Latest 3 | 4 | ## Features 5 | * A user-friendly description of a new feature. {issue-number} 6 | 7 | ## Fixes 8 | * A user-friendly description of a fix. {issue-number} 9 | 10 | ## Security 11 | * A user-friendly description of a security fix. {issue-number} 12 | 13 | --- 14 | 15 | [//]: # (START/v1.8.1) 16 | # v1.8.1 17 | 18 | ## Fixes 19 | * Upgrade operator to use Operator SDK v1.33.0. {#180} 20 | 21 | --- 22 | 23 | [//]: # (START/v1.8.0) 24 | # v1.8.0 25 | 26 | ## Features 27 | * Added volume projected detection. Credit to @mmorejon. {#168} 28 | 29 | --- 30 | 31 | [//]: # (START/v1.7.1) 32 | # v1.7.1 33 | 34 | ## Fixes 35 | * Adjusting logging level on various logs to reduce unnecessary logging. {#164} 36 | 37 | --- 38 | 39 | [//]: # (START/v1.7.0) 40 | # v1.7.0 41 | 42 | ## Features 43 | * Upgraded operator to version 1.29.0. {#162} 44 | * Upgraded Golang version to 1.20. {#161} 45 | * Upgraded 1Password Connect version to 1.5.1. {#161} 46 | * Added runAsNonRoot and allowPrivalegeEscalation to specs. {#151} 47 | * Added code quality improvements. {#146} 48 | 49 | --- 50 | 51 | [//]: # (START/v1.6.0) 52 | # v1.6.0 53 | 54 | This version of the operator highlights the migration of the operator 55 | to use the latest version of the `operator-sdk` (`1.25.0` at the time of this release). 56 | 57 | For the users, this shouldn't affect the functionality of the operator. 58 | 59 | This migration enables us to use the new project structure, as well as updated packages that enables 60 | the team (as well as the contributors) to develop the operator more effective. 61 | 62 | ## Features 63 | * Migrate the operator to use the latest `operator-sdk` {#124} 64 | 65 | --- 66 | 67 | [//]: # (START/v1.5.0) 68 | # v1.5.0 69 | 70 | ## Features 71 | * `OnePasswordItem` now contains a `status` which contains the status of creating the kubernetes secret for a OnePasswordItem. {#52} 72 | 73 | ## Fixes 74 | * The operator no longer logs an error about changing the secret type if the secret type is not actually being changed. 75 | * Annotations on a deployment are no longer removed when the operator triggers a restart. {#112} 76 | 77 | --- 78 | 79 | [//]: # "START/v1.4.1" 80 | 81 | # v1.4.1 82 | 83 | ## Fixes 84 | 85 | - OwnerReferences on secrets are now persisted after an item is updated. {#101} 86 | - Annotations from a Deployment or OnePasswordItem are no longer applied to Secrets that are created for it. {#102} 87 | 88 | --- 89 | 90 | [//]: # "START/v1.4.0" 91 | 92 | # v1.4.0 93 | 94 | ## Features 95 | 96 | - The operator now declares the an OwnerReference for the secrets it manages. This should stop secrets from getting pruned by tools like Argo CD. {#51,#84,#96} 97 | 98 | --- 99 | 100 | [//]: # "START/v1.3.0" 101 | 102 | # v1.3.0 103 | 104 | ## Features 105 | 106 | - Added support for loading secrets from files stored in 1Password. {#47} 107 | 108 | --- 109 | 110 | [//]: # "START/v1.2.0" 111 | 112 | # v1.2.0 113 | 114 | ## Features 115 | 116 | - Support secrets provisioned through FromEnv. {#74} 117 | - Support configuration of Kubernetes Secret type. {#87} 118 | - Improved logging. (#72) 119 | 120 | --- 121 | 122 | [//]: # "START/v1.1.0" 123 | 124 | # v1.1.0 125 | 126 | ## Fixes 127 | 128 | - Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66} 129 | 130 | --- 131 | 132 | [//]: # "START/v1.0.2" 133 | 134 | # v1.0.2 135 | 136 | ## Fixes 137 | 138 | - Name normalizer added to handle non-conforming item names. 139 | 140 | --- 141 | 142 | [//]: # "START/v1.0.1" 143 | 144 | # v1.0.1 145 | 146 | ## Features 147 | 148 | - This release also contains an arm64 Docker image. {#20} 149 | - Docker images are also pushed to the :latest and :. tags. 150 | 151 | --- 152 | 153 | [//]: # "START/v1.0.0" 154 | 155 | # v1.0.0 156 | 157 | ## Features: 158 | 159 | - Option to automatically deploy 1Password Connect via the operator 160 | - Ignore restart annotation when looking for 1Password annotations 161 | - Release Automation 162 | - Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource 163 | - Adding configuration for auto rolling restart on deployments 164 | - Configure Auto Restarts for a OnePasswordItem Custom Resource 165 | - Update Connect Dependencies to latest 166 | - Add Github action for building and testing operator 167 | 168 | ## Fixes: 169 | 170 | - Fix spec field example for OnePasswordItem in readme 171 | - Casing of annotations are now consistent 172 | 173 | --- 174 | 175 | [//]: # "START/v0.0.2" 176 | 177 | # v0.0.2 178 | 179 | ## Features: 180 | 181 | - Items can now be accessed by either `vaults//items/` or `vaults//items/` 182 | 183 | --- 184 | 185 | [//]: # "START/v0.0.1" 186 | 187 | # v0.0.1 188 | 189 | Initial 1Password Operator release 190 | 191 | ## Features 192 | 193 | - watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret 194 | - watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets 195 | - watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes 196 | - restart pods when secret has been updated 197 | - cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted 198 | 199 | --- 200 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to the 1Password Kubernetes Operator project 👋! Before you start, please take a moment to read through this guide to understand our contribution process. 4 | 5 | ## Testing 6 | 7 | - For functional testing, run the local version of the operator. From the project root: 8 | 9 | ```sh 10 | # Go to the K8s environment (e.g. minikube) 11 | eval $(minikube docker-env) 12 | 13 | # Build the local Docker image for the operator 14 | make docker-build 15 | 16 | # Deploy the operator 17 | make deploy 18 | 19 | # Remove the operator from K8s 20 | make undeploy 21 | ``` 22 | 23 | - For testing the changes made to the `OnePasswordItem` Custom Resource Definition (CRD), you need to re-generate the object: 24 | ```sh 25 | make manifests 26 | ``` 27 | 28 | - Run tests for the operator: 29 | 30 | ```sh 31 | make test 32 | ``` 33 | 34 | You can check other available commands that may come in handy by running `make help`. 35 | 36 | ## Debugging 37 | 38 | - Running `kubectl describe pod` will fetch details about pods. This includes configuration information about the container(s) and Pod (labels, resource requirements, etc) and status information about the container(s) and Pod (state, readiness, restart count, events, etc.). 39 | - Running `kubectl logs ${POD_NAME} ${CONTAINER_NAME}` will print the logs from the container(s) in a pod. This can help with debugging issues by inspection. 40 | - Running `kubectl exec ${POD_NAME} -c ${CONTAINER_NAME} -- ${CMD}` allows executing a command inside a specific container. 41 | 42 | For more debugging documentation, see: https://kubernetes.io/docs/tasks/debug/debug-application/debug-pods/ 43 | 44 | ## Documentation Updates 45 | 46 | If applicable, update the [USAGEGUIDE.md](./USAGEGUIDE.md) and [README.md](./README.md) to reflect any changes introduced by the new code. 47 | 48 | ## Sign your commits 49 | 50 | To get your PR merged, we require you to sign your commits. There are three options you can choose from. 51 | 52 | ### Sign commits with 1Password 53 | 54 | You can sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process. 55 | 56 | Learn how to use [1Password to sign your commits](https://developer.1password.com/docs/ssh/git-commit-signing/). 57 | 58 | ### Sign commits with ssh-agent 59 | 60 | Follow the steps below to set up commit signing with `ssh-agent`: 61 | 62 | 1. [Generate an SSH key and add it to ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) 63 | 2. [Add the SSH key to your GitHub account](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) 64 | 3. [Configure git to use your SSH key for commits signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-ssh-key) 65 | 66 | ### Sign commits with gpg 67 | 68 | Follow the steps below to set up commit signing with `gpg`: 69 | 70 | 1. [Generate a GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) 71 | 2. [Add the GPG key to your GitHub account](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account) 72 | 3. [Configure git to use your GPG key for commits signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-gpg-key) 73 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.21 as builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | 11 | # Copy the go source 12 | COPY cmd/main.go cmd/main.go 13 | COPY api/ api/ 14 | COPY internal/controller/ internal/controller/ 15 | COPY pkg/ pkg/ 16 | COPY version/ version/ 17 | COPY vendor/ vendor/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 \ 25 | GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ 26 | go build \ 27 | -ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \ 28 | -mod vendor \ 29 | -a -o manager cmd/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 gcr.io/distroless/static:nonroot 34 | WORKDIR / 35 | COPY --from=builder /workspace/manager . 36 | USER 65532:65532 37 | COPY config/connect/ config/connect/ 38 | 39 | ENTRYPOINT ["/manager"] 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 1Password 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: onepassword.com 6 | layout: 7 | - go.kubebuilder.io/v4 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: onepassword-operator 12 | repo: github.com/1Password/onepassword-operator 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | namespaced: true 17 | controller: true 18 | domain: onepassword.com 19 | kind: OnePasswordItem 20 | path: github.com/1Password/onepassword-operator/api/v1 21 | version: v1 22 | version: "3" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

1Password Connect Kubernetes Operator

6 |

Integrate 1Password Connect with your Kubernetes Infrastructure

7 | 8 | Get started 9 | 10 |
11 | 12 | --- 13 | 14 | The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes Secrets with 1Password. The operator also handles autorestarting deployments when 1Password items are updated. 15 | 16 | ## ✨ Get started 17 | 18 | ### 🚀 Quickstart 19 | 20 | 1. Add the [1Password Helm Chart](https://github.com/1Password/connect-helm-charts) to your repository. 21 | 22 | 2. Run the following command to install Connect and the 1Password Kubernetes Operator in your infrastructure: 23 | 24 | ``` 25 | helm install connect 1password/connect --set-file connect.credentials=1password-credentials-demo.json --set operator.create=true --set operator.token.value = 26 | ``` 27 | 28 | 3. Create a Kubernetes Secret from a 1Password item: 29 | 30 | ``` 31 | apiVersion: onepassword.com/v1 32 | kind: OnePasswordItem 33 | metadata: 34 | name: #this name will also be used for naming the generated kubernetes secret 35 | spec: 36 | itemPath: "vaults//items/" 37 | ``` 38 | 39 | Deploy the OnePasswordItem to Kubernetes: 40 | 41 | ``` 42 | kubectl apply -f .yaml 43 | ``` 44 | 45 | Check that the Kubernetes Secret has been generated: 46 | 47 | ``` 48 | kubectl get secret 49 | ``` 50 | 51 | ### 📄 Usage 52 | 53 | Refer to the [Usage Guide](USAGEGUIDE.md) for documentation on how to deploy and use the 1Password Operator. 54 | 55 | ## 💙 Community & Support 56 | 57 | - File an [issue](https://github.com/1Password/onepassword-operator/issues) for bugs and feature requests. 58 | - Join the [Developer Slack workspace](https://join.slack.com/t/1password-devs/shared_invite/zt-1halo11ps-6o9pEv96xZ3LtX_VE0fJQA). 59 | - Subscribe to the [Developer Newsletter](https://1password.com/dev-subscribe/). 60 | 61 | ## 🔐 Security 62 | 63 | 1Password requests you practice responsible disclosure if you discover a vulnerability. 64 | 65 | Please file requests by sending an email to bugbounty@agilebits.com. 66 | -------------------------------------------------------------------------------- /USAGEGUIDE.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Usage Guide

4 |
5 | 6 | ## Table of Contents 7 | 8 | - [Prerequisites](#prerequisites) 9 | - [Deploying 1Password Connect to Kubernetes](#deploying-1password-connect-to-kubernetes) 10 | - [Kubernetes Operator Deployment](#kubernetes-operator-deployment) 11 | - [Usage](#usage) 12 | - [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments) 13 | - [Development](#development) 14 | 15 | ## Prerequisites 16 | 17 | - [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/) 18 | - [`kubectl` installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 19 | - [`docker` installed](https://docs.docker.com/get-docker/) 20 | - [A `1password-credentials.json` file generated and a 1Password Connect API Token issued for the K8s Operator integration](https://developer.1password.com/docs/connect/get-started/#step-1-set-up-a-secrets-automation-workflow) 21 | 22 | ## Deploying 1Password Connect to Kubernetes 23 | 24 | If 1Password Connect is already running, you can skip this step. 25 | 26 | There are options to deploy 1Password Connect: 27 | 28 | - [Deploy with Helm](#deploy-with-helm) 29 | - [Deploy using the Connect Operator](#deploy-using-the-connect-operator) 30 | 31 | ### Deploy with Helm 32 | 33 | The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes. 34 | 35 | [The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts) 36 | 37 | ### Deploy using the Connect Operator 38 | 39 | This guide will provide a quickstart option for deploying a default configuration of 1Password Connect via starting the deploying the 1Password Connect Operator, however, it is recommended that you instead deploy your own manifest file if customization of the 1Password Connect deployment is desired. 40 | 41 | Encode the `1password-credentials.json` file you generated in the prerequisite steps and save it to a file named `op-session`: 42 | 43 | ```bash 44 | cat 1password-credentials.json | base64 | \ 45 | tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session 46 | ``` 47 | 48 | Create a Kubernetes secret from the op-session file: 49 | 50 | ```bash 51 | kubectl create secret generic op-credentials --from-file=op-session 52 | ``` 53 | 54 | Add the following environment variable to the onepassword-connect-operator container in `/config/manager/manager.yaml`: 55 | 56 | ```yaml 57 | - name: MANAGE_CONNECT 58 | value: "true" 59 | ``` 60 | 61 | Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the current namespace. 62 | 63 | ### Kubernetes Operator Deployment 64 | 65 | #### Create Kubernetes Secret for OP_CONNECT_TOKEN #### 66 | 67 | Create a Connect token for the operator and save it as a Kubernetes Secret: 68 | 69 | ```bash 70 | kubectl create secret generic onepassword-token --from-literal=token="" 71 | ``` 72 | 73 | If you do not have a token for the operator, you can generate a token and save it to Kubernetes with the following command: 74 | 75 | ```bash 76 | kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token op-k8s-operator --vault ) 77 | ``` 78 | 79 | **Deploying the Operator** 80 | 81 | An sample Deployment yaml can be found at `/config/manager/manager.yaml`. 82 | 83 | To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml: 84 | 85 | - **OP_CONNECT_HOST** *(required)*: Specifies the host name within Kubernetes in which to access the 1Password Connect. 86 | - **WATCH_NAMESPACE:** *(default: watch all namespaces)*: Comma separated list of what Namespaces to watch for changes. 87 | - **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. 88 | - **MANAGE_CONNECT** *(default: false)*: If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the current namespace. 89 | - **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section. 90 | 91 | You can also set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. (Note: the default value is `debug`.) 92 | 93 | Example: 94 | ```yaml 95 | . 96 | . 97 | . 98 | containers: 99 | - command: 100 | - /manager 101 | args: 102 | - --leader-elect 103 | - --zap-log-level=info 104 | image: 1password/onepassword-operator:latest 105 | . 106 | . 107 | . 108 | ``` 109 | To deploy the operator, simply run the following command: 110 | 111 | ```shell 112 | make deploy 113 | ``` 114 | 115 | **Undeploy Operator** 116 | 117 | ``` 118 | make undeploy 119 | ``` 120 | 121 | ## Usage 122 | 123 | To create a Kubernetes Secret from a 1Password item, create a yaml file with the following 124 | 125 | ```yaml 126 | apiVersion: onepassword.com/v1 127 | kind: OnePasswordItem 128 | metadata: 129 | name: #this name will also be used for naming the generated kubernetes secret 130 | spec: 131 | itemPath: "vaults//items/" 132 | ``` 133 | 134 | Deploy the OnePasswordItem to Kubernetes: 135 | 136 | ```bash 137 | kubectl apply -f .yaml 138 | ``` 139 | 140 | To test that the Kubernetes Secret check that the following command returns a secret: 141 | 142 | ```bash 143 | kubectl get secret 144 | ``` 145 | 146 | **Note:** Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret. 147 | 148 | To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata: 149 | 150 | ```yaml 151 | apiVersion: apps/v1 152 | kind: Deployment 153 | metadata: 154 | name: deployment-example 155 | annotations: 156 | operator.1password.io/item-path: "vaults//items/" 157 | operator.1password.io/item-name: "" 158 | ``` 159 | 160 | Applying this yaml file will create a Kubernetes Secret with the name `` and contents from the location specified at the specified Item Path. 161 | 162 | The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password. 163 | In case of fields that store files, the file's contents will be used as the value. 164 | 165 | Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence. 166 | 167 | **Note:** Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret. 168 | 169 | If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes. 170 | 171 | --- 172 | 173 | **NOTE** 174 | 175 | If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item. 176 | 177 | Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format: 178 | 179 | - Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed 180 | - All whitespaces between words will be replaced by `-` 181 | - All the letters will be lower-cased. 182 | 183 | --- 184 | 185 | ## Configuring Automatic Rolling Restarts of Deployments 186 | 187 | If a 1Password Item that is linked to a Kubernetes Secret is updated, any deployments configured to `auto-restart` AND are using that secret will be given a rolling restart the next time 1Password Connect is polled for updates. 188 | 189 | There are many levels of granularity on which to configure auto restarts on deployments: 190 | - Operator level 191 | - Per-namespace 192 | - Per-deployment 193 | 194 | **Operator Level**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environment variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false. 195 | 196 | **Per Namespace**: This method allows for managing auto restarts on all deployments within a namespace. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired namespace. An example of this is shown below: 197 | 198 | ```yaml 199 | # enabled auto restarts for all deployments within a namespace unless overwritten within a deployment 200 | apiVersion: v1 201 | kind: Namespace 202 | metadata: 203 | name: "example-namespace" 204 | annotations: 205 | operator.1password.io/auto-restart: "true" 206 | ``` 207 | 208 | If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment. 209 | 210 | **Per Deployment** 211 | This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below: 212 | 213 | ```yaml 214 | # enabled auto restarts for the deployment 215 | apiVersion: v1 216 | kind: Deployment 217 | metadata: 218 | name: "example-deployment" 219 | annotations: 220 | operator.1password.io/auto-restart: "true" 221 | ``` 222 | 223 | If the value is not set, the auto restart settings on the namespace will be used. 224 | 225 | **Per OnePasswordItem Custom Resource** 226 | This method allows for managing auto restarts on a given OnePasswordItem custom resource. Auto restarts can by managed by setting the annotation `operator.1password.io/auto_restart` to either `true` or `false` on the desired OnePasswordItem. An example of this is shown below: 227 | 228 | ```yaml 229 | # enabled auto restarts for the OnePasswordItem 230 | apiVersion: onepassword.com/v1 231 | kind: OnePasswordItem 232 | metadata: 233 | name: example 234 | annotations: 235 | operator.1password.io/auto-restart: "true" 236 | ``` 237 | 238 | If the value is not set, the auto restart settings on the deployment will be used. 239 | 240 | ## Development 241 | 242 | ### How it works 243 | 244 | This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 245 | 246 | It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) 247 | which provides a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster 248 | 249 | ### Test It Out 250 | 251 | 1. Install the CRDs into the cluster: 252 | 253 | ```sh 254 | make install 255 | ``` 256 | 257 | 2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): 258 | 259 | ```sh 260 | make run 261 | ``` 262 | 263 | **NOTE:** You can also run this in one step by running: `make install run` 264 | 265 | ### Modifying the API definitions 266 | 267 | If you are editing the API definitions, generate the manifests such as CRs or CRDs using: 268 | 269 | ```sh 270 | make manifests 271 | ``` 272 | 273 | **NOTE:** Run `make --help` for more information on all potential `make` targets 274 | 275 | More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020-2024 1Password 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | // Package v1 contains API Schema definitions for the v1 API group 26 | // +kubebuilder:object:generate=true 27 | // +groupName=onepassword.com 28 | package v1 29 | 30 | import ( 31 | "k8s.io/apimachinery/pkg/runtime/schema" 32 | "sigs.k8s.io/controller-runtime/pkg/scheme" 33 | ) 34 | 35 | var ( 36 | // GroupVersion is group version used to register these objects 37 | GroupVersion = schema.GroupVersion{Group: "onepassword.com", Version: "v1"} 38 | 39 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 40 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 41 | 42 | // AddToScheme adds the types in this group-version to the given scheme. 43 | AddToScheme = SchemeBuilder.AddToScheme 44 | ) 45 | -------------------------------------------------------------------------------- /api/v1/onepassworditem_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020-2024 1Password 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package v1 26 | 27 | import ( 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | ) 30 | 31 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 32 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 33 | 34 | // OnePasswordItemSpec defines the desired state of OnePasswordItem 35 | type OnePasswordItemSpec struct { 36 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 37 | // Important: Run "make" to regenerate code after modifying this file 38 | 39 | ItemPath string `json:"itemPath,omitempty"` 40 | } 41 | 42 | type OnePasswordItemConditionType string 43 | 44 | const ( 45 | // OnePasswordItemReady means the Kubernetes secret is ready for use. 46 | OnePasswordItemReady OnePasswordItemConditionType = "Ready" 47 | ) 48 | 49 | type OnePasswordItemCondition struct { 50 | // Type of job condition, Completed. 51 | Type OnePasswordItemConditionType `json:"type"` 52 | // Status of the condition, one of True, False, Unknown. 53 | Status metav1.ConditionStatus `json:"status"` 54 | // Last time the condition transit from one status to another. 55 | // +optional 56 | LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` 57 | // Human-readable message indicating details about last transition. 58 | // +optional 59 | Message string `json:"message,omitempty"` 60 | } 61 | 62 | // OnePasswordItemStatus defines the observed state of OnePasswordItem 63 | type OnePasswordItemStatus struct { 64 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 65 | // Important: Run "make" to regenerate code after modifying this file 66 | 67 | Conditions []OnePasswordItemCondition `json:"conditions"` 68 | } 69 | 70 | //+kubebuilder:object:root=true 71 | //+kubebuilder:subresource:status 72 | 73 | // OnePasswordItem is the Schema for the onepassworditems API 74 | type OnePasswordItem struct { 75 | metav1.TypeMeta `json:",inline"` 76 | metav1.ObjectMeta `json:"metadata,omitempty"` 77 | 78 | // Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types 79 | Type string `json:"type,omitempty"` 80 | Spec OnePasswordItemSpec `json:"spec,omitempty"` 81 | Status OnePasswordItemStatus `json:"status,omitempty"` 82 | } 83 | 84 | //+kubebuilder:object:root=true 85 | 86 | // OnePasswordItemList contains a list of OnePasswordItem 87 | type OnePasswordItemList struct { 88 | metav1.TypeMeta `json:",inline"` 89 | metav1.ListMeta `json:"metadata,omitempty"` 90 | Items []OnePasswordItem `json:"items"` 91 | } 92 | 93 | func init() { 94 | SchemeBuilder.Register(&OnePasswordItem{}, &OnePasswordItemList{}) 95 | } 96 | -------------------------------------------------------------------------------- /api/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | MIT License 5 | 6 | Copyright (c) 2020-2024 1Password 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | // Code generated by controller-gen. DO NOT EDIT. 28 | 29 | package v1 30 | 31 | import ( 32 | runtime "k8s.io/apimachinery/pkg/runtime" 33 | ) 34 | 35 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 36 | func (in *OnePasswordItem) DeepCopyInto(out *OnePasswordItem) { 37 | *out = *in 38 | out.TypeMeta = in.TypeMeta 39 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 40 | out.Spec = in.Spec 41 | in.Status.DeepCopyInto(&out.Status) 42 | } 43 | 44 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem. 45 | func (in *OnePasswordItem) DeepCopy() *OnePasswordItem { 46 | if in == nil { 47 | return nil 48 | } 49 | out := new(OnePasswordItem) 50 | in.DeepCopyInto(out) 51 | return out 52 | } 53 | 54 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 55 | func (in *OnePasswordItem) DeepCopyObject() runtime.Object { 56 | if c := in.DeepCopy(); c != nil { 57 | return c 58 | } 59 | return nil 60 | } 61 | 62 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 63 | func (in *OnePasswordItemCondition) DeepCopyInto(out *OnePasswordItemCondition) { 64 | *out = *in 65 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemCondition. 69 | func (in *OnePasswordItemCondition) DeepCopy() *OnePasswordItemCondition { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(OnePasswordItemCondition) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 79 | func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) { 80 | *out = *in 81 | out.TypeMeta = in.TypeMeta 82 | in.ListMeta.DeepCopyInto(&out.ListMeta) 83 | if in.Items != nil { 84 | in, out := &in.Items, &out.Items 85 | *out = make([]OnePasswordItem, len(*in)) 86 | for i := range *in { 87 | (*in)[i].DeepCopyInto(&(*out)[i]) 88 | } 89 | } 90 | } 91 | 92 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemList. 93 | func (in *OnePasswordItemList) DeepCopy() *OnePasswordItemList { 94 | if in == nil { 95 | return nil 96 | } 97 | out := new(OnePasswordItemList) 98 | in.DeepCopyInto(out) 99 | return out 100 | } 101 | 102 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 103 | func (in *OnePasswordItemList) DeepCopyObject() runtime.Object { 104 | if c := in.DeepCopy(); c != nil { 105 | return c 106 | } 107 | return nil 108 | } 109 | 110 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 111 | func (in *OnePasswordItemSpec) DeepCopyInto(out *OnePasswordItemSpec) { 112 | *out = *in 113 | } 114 | 115 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemSpec. 116 | func (in *OnePasswordItemSpec) DeepCopy() *OnePasswordItemSpec { 117 | if in == nil { 118 | return nil 119 | } 120 | out := new(OnePasswordItemSpec) 121 | in.DeepCopyInto(out) 122 | return out 123 | } 124 | 125 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 126 | func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) { 127 | *out = *in 128 | if in.Conditions != nil { 129 | in, out := &in.Conditions, &out.Conditions 130 | *out = make([]OnePasswordItemCondition, len(*in)) 131 | for i := range *in { 132 | (*in)[i].DeepCopyInto(&(*out)[i]) 133 | } 134 | } 135 | } 136 | 137 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus. 138 | func (in *OnePasswordItemStatus) DeepCopy() *OnePasswordItemStatus { 139 | if in == nil { 140 | return nil 141 | } 142 | out := new(OnePasswordItemStatus) 143 | in.DeepCopyInto(out) 144 | return out 145 | } 146 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020-2024 1Password 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package main 26 | 27 | import ( 28 | "errors" 29 | "flag" 30 | "fmt" 31 | "os" 32 | "regexp" 33 | "runtime" 34 | "strconv" 35 | "strings" 36 | "time" 37 | 38 | "github.com/1Password/connect-sdk-go/connect" 39 | 40 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 41 | // to ensure that exec-entrypoint and run can make use of them. 42 | _ "k8s.io/client-go/plugin/pkg/client/auth" 43 | 44 | k8sruntime "k8s.io/apimachinery/pkg/runtime" 45 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 46 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 47 | "k8s.io/client-go/rest" 48 | ctrl "sigs.k8s.io/controller-runtime" 49 | "sigs.k8s.io/controller-runtime/pkg/cache" 50 | "sigs.k8s.io/controller-runtime/pkg/healthz" 51 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 52 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 53 | 54 | onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1" 55 | "github.com/1Password/onepassword-operator/internal/controller" 56 | op "github.com/1Password/onepassword-operator/pkg/onepassword" 57 | "github.com/1Password/onepassword-operator/pkg/utils" 58 | "github.com/1Password/onepassword-operator/version" 59 | //+kubebuilder:scaffold:imports 60 | ) 61 | 62 | var ( 63 | scheme = k8sruntime.NewScheme() 64 | setupLog = ctrl.Log.WithName("setup") 65 | ) 66 | 67 | const ( 68 | envPollingIntervalVariable = "POLLING_INTERVAL" 69 | manageConnect = "MANAGE_CONNECT" 70 | restartDeploymentsEnvVariable = "AUTO_RESTART" 71 | defaultPollingInterval = 600 72 | 73 | annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" 74 | ) 75 | 76 | // Change below variables to serve metrics on different host or port. 77 | var ( 78 | metricsHost = "0.0.0.0" 79 | metricsPort int32 = 8383 80 | operatorMetricsPort int32 = 8686 81 | ) 82 | 83 | func printVersion() { 84 | setupLog.Info(fmt.Sprintf("Operator Version: %s", version.OperatorVersion)) 85 | setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) 86 | setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) 87 | setupLog.Info(fmt.Sprintf("Version of operator-sdk: %v", version.OperatorSDKVersion)) 88 | } 89 | 90 | func init() { 91 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 92 | 93 | utilruntime.Must(onepasswordcomv1.AddToScheme(scheme)) 94 | //+kubebuilder:scaffold:scheme 95 | } 96 | 97 | func main() { 98 | var metricsAddr string 99 | var enableLeaderElection bool 100 | var probeAddr string 101 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 102 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 103 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 104 | "Enable leader election for controller manager. "+ 105 | "Enabling this will ensure there is only one active controller manager.") 106 | opts := zap.Options{ 107 | Development: true, 108 | } 109 | opts.BindFlags(flag.CommandLine) 110 | flag.Parse() 111 | 112 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 113 | 114 | printVersion() 115 | 116 | watchNamespace, err := getWatchNamespace() 117 | if err != nil { 118 | setupLog.Error(err, "unable to get WatchNamespace, "+ 119 | "the manager will watch and manage resources in all namespaces") 120 | } 121 | 122 | deploymentNamespace, err := utils.GetOperatorNamespace() 123 | if err != nil { 124 | setupLog.Error(err, "Failed to get namespace") 125 | os.Exit(1) 126 | } 127 | 128 | options := ctrl.Options{ 129 | Scheme: scheme, 130 | Metrics: metricsserver.Options{BindAddress: metricsAddr}, 131 | HealthProbeBindAddress: probeAddr, 132 | LeaderElection: enableLeaderElection, 133 | LeaderElectionID: "c26807fd.onepassword.com", 134 | } 135 | 136 | // Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2) 137 | if watchNamespace != "" { 138 | namespaces := strings.Split(watchNamespace, ",") 139 | namespaceMap := make(map[string]cache.Config) 140 | for _, namespace := range namespaces { 141 | namespaceMap[namespace] = cache.Config{} 142 | } 143 | options.NewCache = func(config *rest.Config, opts cache.Options) (cache.Cache, error) { 144 | opts.DefaultNamespaces = namespaceMap 145 | return cache.New(config, opts) 146 | } 147 | } 148 | 149 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options) 150 | if err != nil { 151 | setupLog.Error(err, "unable to start manager") 152 | os.Exit(1) 153 | } 154 | 155 | // Setup One Password Client 156 | opConnectClient, err := connect.NewClientFromEnvironment() 157 | if err != nil { 158 | setupLog.Error(err, "unable to create Connect client") 159 | os.Exit(1) 160 | } 161 | 162 | if err = (&controller.OnePasswordItemReconciler{ 163 | Client: mgr.GetClient(), 164 | Scheme: mgr.GetScheme(), 165 | OpConnectClient: opConnectClient, 166 | }).SetupWithManager(mgr); err != nil { 167 | setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem") 168 | os.Exit(1) 169 | } 170 | 171 | r, _ := regexp.Compile(annotationRegExpString) 172 | if err = (&controller.DeploymentReconciler{ 173 | Client: mgr.GetClient(), 174 | Scheme: mgr.GetScheme(), 175 | OpConnectClient: opConnectClient, 176 | OpAnnotationRegExp: r, 177 | }).SetupWithManager(mgr); err != nil { 178 | setupLog.Error(err, "unable to create controller", "controller", "Deployment") 179 | os.Exit(1) 180 | } 181 | //+kubebuilder:scaffold:builder 182 | 183 | //Setup 1PasswordConnect 184 | if shouldManageConnect() { 185 | setupLog.Info("Automated Connect Management Enabled") 186 | go func() { 187 | connectStarted := false 188 | for connectStarted == false { 189 | err := op.SetupConnect(mgr.GetClient(), deploymentNamespace) 190 | // Cache Not Started is an acceptable error. Retry until cache is started. 191 | if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { 192 | setupLog.Error(err, "") 193 | os.Exit(1) 194 | } 195 | if err == nil { 196 | connectStarted = true 197 | } 198 | } 199 | }() 200 | } else { 201 | setupLog.Info("Automated Connect Management Disabled") 202 | } 203 | 204 | // Setup update secrets task 205 | updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments()) 206 | done := make(chan bool) 207 | ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets()) 208 | go func() { 209 | for { 210 | select { 211 | case <-done: 212 | ticker.Stop() 213 | return 214 | case <-ticker.C: 215 | err := updatedSecretsPoller.UpdateKubernetesSecretsTask() 216 | if err != nil { 217 | setupLog.Error(err, "error running update kubernetes secret task") 218 | } 219 | } 220 | } 221 | }() 222 | 223 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 224 | setupLog.Error(err, "unable to set up health check") 225 | os.Exit(1) 226 | } 227 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 228 | setupLog.Error(err, "unable to set up ready check") 229 | os.Exit(1) 230 | } 231 | 232 | setupLog.Info("starting manager") 233 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 234 | setupLog.Error(err, "problem running manager") 235 | os.Exit(1) 236 | } 237 | } 238 | 239 | // getWatchNamespace returns the Namespace the operator should be watching for changes 240 | func getWatchNamespace() (string, error) { 241 | // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE 242 | // which specifies the Namespace to watch. 243 | // An empty value means the operator is running with cluster scope. 244 | var watchNamespaceEnvVar = "WATCH_NAMESPACE" 245 | 246 | ns, found := os.LookupEnv(watchNamespaceEnvVar) 247 | if !found { 248 | return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar) 249 | } 250 | return ns, nil 251 | } 252 | 253 | func shouldManageConnect() bool { 254 | shouldManageConnect, found := os.LookupEnv(manageConnect) 255 | if found { 256 | shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect)) 257 | if err != nil { 258 | setupLog.Error(err, "") 259 | os.Exit(1) 260 | } 261 | return shouldManageConnectBool 262 | } 263 | return false 264 | } 265 | 266 | func shouldAutoRestartDeployments() bool { 267 | shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable) 268 | if found { 269 | shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments)) 270 | if err != nil { 271 | setupLog.Error(err, "") 272 | os.Exit(1) 273 | } 274 | return shouldAutoRestartDeploymentsBool 275 | } 276 | return false 277 | } 278 | 279 | func getPollingIntervalForUpdatingSecrets() time.Duration { 280 | timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable) 281 | if found { 282 | timeInSeconds, err := strconv.Atoi(timeInSecondsString) 283 | if err == nil { 284 | return time.Duration(timeInSeconds) * time.Second 285 | } 286 | setupLog.Info("Invalid value set for polling interval. Must be a valid integer.") 287 | } 288 | 289 | setupLog.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval)) 290 | return time.Duration(defaultPollingInterval) * time.Second 291 | } 292 | -------------------------------------------------------------------------------- /config/connect/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: onepassword-connect 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: onepassword-connect 9 | template: 10 | metadata: 11 | labels: 12 | app: onepassword-connect 13 | version: "1.0.0" 14 | spec: 15 | securityContext: 16 | runAsNonRoot: true 17 | volumes: 18 | - name: shared-data 19 | emptyDir: {} 20 | - name: credentials 21 | secret: 22 | secretName: op-credentials 23 | initContainers: 24 | - name: sqlite-permissions 25 | image: alpine:3.12 26 | command: 27 | - "/bin/sh" 28 | - "-c" 29 | args: 30 | - "mkdir -p /home/opuser/.op/data && chown -R 999 /home/opuser && chmod -R 700 /home/opuser && chmod -f -R 600 /home/opuser/.op/config || :" 31 | volumeMounts: 32 | - mountPath: /home/opuser/.op/data 33 | name: shared-data 34 | containers: 35 | - name: connect-api 36 | image: 1password/connect-api:latest 37 | securityContext: 38 | allowPrivilegeEscalation: false 39 | resources: 40 | limits: 41 | memory: "128Mi" 42 | requests: 43 | cpu: "0.2" 44 | ports: 45 | - containerPort: 8080 46 | env: 47 | - name: OP_SESSION 48 | valueFrom: 49 | secretKeyRef: 50 | name: op-credentials 51 | key: op-session 52 | volumeMounts: 53 | - mountPath: /home/opuser/.op/data 54 | name: shared-data 55 | - name: connect-sync 56 | image: 1password/connect-sync:latest 57 | securityContext: 58 | allowPrivilegeEscalation: false 59 | resources: 60 | limits: 61 | memory: "128Mi" 62 | requests: 63 | cpu: "0.2" 64 | ports: 65 | - containerPort: 8081 66 | env: 67 | - name: OP_HTTP_PORT 68 | value: "8081" 69 | - name: OP_SESSION 70 | valueFrom: 71 | secretKeyRef: 72 | name: op-credentials 73 | key: op-session 74 | volumeMounts: 75 | - mountPath: /home/opuser/.op/data 76 | name: shared-data 77 | -------------------------------------------------------------------------------- /config/connect/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: onepassword-connect 5 | spec: 6 | type: NodePort 7 | selector: 8 | app: onepassword-connect 9 | ports: 10 | - port: 8080 11 | name: connect-api 12 | nodePort: 30080 13 | - port: 8081 14 | name: connect-sync 15 | nodePort: 30081 16 | -------------------------------------------------------------------------------- /config/crd/bases/onepassword.com_onepassworditems.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.13.0 7 | name: onepassworditems.onepassword.com 8 | spec: 9 | group: onepassword.com 10 | names: 11 | kind: OnePasswordItem 12 | listKind: OnePasswordItemList 13 | plural: onepassworditems 14 | singular: onepassworditem 15 | scope: Namespaced 16 | versions: 17 | - name: v1 18 | schema: 19 | openAPIV3Schema: 20 | description: OnePasswordItem is the Schema for the onepassworditems API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: OnePasswordItemSpec defines the desired state of OnePasswordItem 36 | properties: 37 | itemPath: 38 | type: string 39 | type: object 40 | status: 41 | description: OnePasswordItemStatus defines the observed state of OnePasswordItem 42 | properties: 43 | conditions: 44 | items: 45 | properties: 46 | lastTransitionTime: 47 | description: Last time the condition transit from one status 48 | to another. 49 | format: date-time 50 | type: string 51 | message: 52 | description: Human-readable message indicating details about 53 | last transition. 54 | type: string 55 | status: 56 | description: Status of the condition, one of True, False, Unknown. 57 | type: string 58 | type: 59 | description: Type of job condition, Completed. 60 | type: string 61 | required: 62 | - status 63 | - type 64 | type: object 65 | type: array 66 | required: 67 | - conditions 68 | type: object 69 | type: 70 | description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types' 71 | type: string 72 | type: object 73 | served: true 74 | storage: true 75 | subresources: 76 | status: {} 77 | -------------------------------------------------------------------------------- /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/onepassword.com_onepassworditems.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- path: patches/webhook_in_onepassworditems.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- path: patches/cainjection_in_onepassworditems.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # [WEBHOOK] To enable webhook, uncomment the following section 20 | # the following config is for teaching kustomize how to do kustomization for CRDs. 21 | 22 | #configurations: 23 | #- kustomizeconfig.yaml 24 | -------------------------------------------------------------------------------- /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 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_onepassworditems.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: onepassworditems.onepassword.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_onepassworditems.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: onepassworditems.onepassword.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | # namespace: onepassword-connect-operator 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: onepassword-connect- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 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 | patches: 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 | - path: 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 | #- path: 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 | #- path: webhookcainjection_patch.yaml 43 | 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 45 | # Uncomment the following replacements to add the cert-manager CA injection annotations 46 | #replacements: 47 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 48 | # kind: Certificate 49 | # group: cert-manager.io 50 | # version: v1 51 | # name: serving-cert # this name should match the one in certificate.yaml 52 | # fieldPath: .metadata.namespace # namespace of the certificate CR 53 | # targets: 54 | # - select: 55 | # kind: ValidatingWebhookConfiguration 56 | # fieldPaths: 57 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 58 | # options: 59 | # delimiter: '/' 60 | # index: 0 61 | # create: true 62 | # - select: 63 | # kind: MutatingWebhookConfiguration 64 | # fieldPaths: 65 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 66 | # options: 67 | # delimiter: '/' 68 | # index: 0 69 | # create: true 70 | # - select: 71 | # kind: CustomResourceDefinition 72 | # fieldPaths: 73 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 74 | # options: 75 | # delimiter: '/' 76 | # index: 0 77 | # create: true 78 | # - source: 79 | # kind: Certificate 80 | # group: cert-manager.io 81 | # version: v1 82 | # name: serving-cert # this name should match the one in certificate.yaml 83 | # fieldPath: .metadata.name 84 | # targets: 85 | # - select: 86 | # kind: ValidatingWebhookConfiguration 87 | # fieldPaths: 88 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 89 | # options: 90 | # delimiter: '/' 91 | # index: 1 92 | # create: true 93 | # - select: 94 | # kind: MutatingWebhookConfiguration 95 | # fieldPaths: 96 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 97 | # options: 98 | # delimiter: '/' 99 | # index: 1 100 | # create: true 101 | # - select: 102 | # kind: CustomResourceDefinition 103 | # fieldPaths: 104 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 105 | # options: 106 | # delimiter: '/' 107 | # index: 1 108 | # create: true 109 | # - source: # Add cert-manager annotation to the webhook Service 110 | # kind: Service 111 | # version: v1 112 | # name: webhook-service 113 | # fieldPath: .metadata.name # namespace of the service 114 | # targets: 115 | # - select: 116 | # kind: Certificate 117 | # group: cert-manager.io 118 | # version: v1 119 | # fieldPaths: 120 | # - .spec.dnsNames.0 121 | # - .spec.dnsNames.1 122 | # options: 123 | # delimiter: '.' 124 | # index: 0 125 | # create: true 126 | # - source: 127 | # kind: Service 128 | # version: v1 129 | # name: webhook-service 130 | # fieldPath: .metadata.namespace # namespace of the service 131 | # targets: 132 | # - select: 133 | # kind: Certificate 134 | # group: cert-manager.io 135 | # version: v1 136 | # fieldPaths: 137 | # - .spec.dnsNames.0 138 | # - .spec.dnsNames.1 139 | # options: 140 | # delimiter: '.' 141 | # index: 1 142 | # create: true 143 | -------------------------------------------------------------------------------- /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: onepassword-connect-operator 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | securityContext: 12 | runAsNonRoot: true 13 | containers: 14 | - name: kube-rbac-proxy 15 | securityContext: 16 | allowPrivilegeEscalation: false 17 | capabilities: 18 | drop: 19 | - "ALL" 20 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 21 | args: 22 | - "--secure-listen-address=0.0.0.0:8443" 23 | - "--upstream=http://127.0.0.1:8080/" 24 | - "--logtostderr=true" 25 | - "--v=0" 26 | ports: 27 | - containerPort: 8443 28 | protocol: TCP 29 | name: https 30 | resources: 31 | limits: 32 | cpu: 500m 33 | memory: 128Mi 34 | requests: 35 | cpu: 5m 36 | memory: 64Mi 37 | - name: manager 38 | args: 39 | - "--health-probe-bind-address=:8081" 40 | - "--metrics-bind-address=127.0.0.1:8080" 41 | - "--leader-elect" 42 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: onepassword-connect-operator 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /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: 1password/onepassword-operator 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: onepassword-connect-operator 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: onepassword-connect-operator 10 | app.kubernetes.io/part-of: onepassword-connect-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: onepassword-connect-operator 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: onepassword-connect-operator 25 | app.kubernetes.io/part-of: onepassword-connect-operator 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | name: onepassword-connect-operator 31 | control-plane: onepassword-connect-operator 32 | replicas: 1 33 | template: 34 | metadata: 35 | annotations: 36 | kubectl.kubernetes.io/default-container: manager 37 | labels: 38 | name: onepassword-connect-operator 39 | control-plane: onepassword-connect-operator 40 | spec: 41 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 42 | # according to the platforms which are supported by your solution. 43 | # It is considered best practice to support multiple architectures. You can 44 | # build your manager image using the makefile target docker-buildx. 45 | # affinity: 46 | # nodeAffinity: 47 | # requiredDuringSchedulingIgnoredDuringExecution: 48 | # nodeSelectorTerms: 49 | # - matchExpressions: 50 | # - key: kubernetes.io/arch 51 | # operator: In 52 | # values: 53 | # - amd64 54 | # - arm64 55 | # - ppc64le 56 | # - s390x 57 | # - key: kubernetes.io/os 58 | # operator: In 59 | # values: 60 | # - linux 61 | securityContext: 62 | runAsNonRoot: true 63 | # TODO(user): For common cases that do not require escalating privileges 64 | # it is recommended to ensure that all your Pods/Containers are restrictive. 65 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 66 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 67 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 68 | # seccompProfile: 69 | # type: RuntimeDefault 70 | containers: 71 | - command: 72 | - /manager 73 | args: 74 | - --leader-elect 75 | image: 1password/onepassword-operator:latest 76 | name: manager 77 | env: 78 | - name: WATCH_NAMESPACE 79 | value: "default" 80 | - name: POD_NAME 81 | valueFrom: 82 | fieldRef: 83 | fieldPath: metadata.name 84 | - name: OPERATOR_NAME 85 | value: "onepassword-connect-operator" 86 | - name: OP_CONNECT_HOST 87 | value: "http://onepassword-connect:8080" 88 | - name: POLLING_INTERVAL 89 | value: "10" 90 | - name: OP_CONNECT_TOKEN 91 | valueFrom: 92 | secretKeyRef: 93 | name: onepassword-token 94 | key: token 95 | - name: AUTO_RESTART 96 | value: "false" 97 | securityContext: 98 | allowPrivilegeEscalation: false 99 | capabilities: 100 | drop: 101 | - "ALL" 102 | livenessProbe: 103 | httpGet: 104 | path: /healthz 105 | port: 8081 106 | initialDelaySeconds: 15 107 | periodSeconds: 20 108 | readinessProbe: 109 | httpGet: 110 | path: /readyz 111 | port: 8081 112 | initialDelaySeconds: 5 113 | periodSeconds: 10 114 | # TODO(user): Configure the resources accordingly based on the project requirements. 115 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 116 | resources: 117 | limits: 118 | cpu: 500m 119 | memory: 128Mi 120 | requests: 121 | cpu: 10m 122 | memory: 64Mi 123 | serviceAccountName: onepassword-connect-operator 124 | terminationGracePeriodSeconds: 10 125 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/onepassword-operator.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | 24 | # path: /spec/template/spec/containers/0/volumeMounts/0 25 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 26 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 27 | # - op: remove 28 | # path: /spec/template/spec/volumes/0 29 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | name: onepassword-connect-operator 7 | control-plane: onepassword-connect-operator 8 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: onepassword-connect-operator 12 | app.kubernetes.io/part-of: onepassword-connect-operator 13 | app.kubernetes.io/managed-by: kustomize 14 | name: onepassword-connect-operator-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | name: onepassword-connect-operator 27 | control-plane: onepassword-connect-operator 28 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: onepassword-connect-operator 9 | app.kubernetes.io/part-of: onepassword-connect-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: onepassword-connect-operator 9 | app.kubernetes.io/part-of: onepassword-connect-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: onepassword-connect-operator 9 | app.kubernetes.io/part-of: onepassword-connect-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: onepassword-connect-operator 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | name: onepassword-connect-operator 6 | control-plane: onepassword-connect-operator 7 | app.kubernetes.io/name: service 8 | app.kubernetes.io/instance: controller-manager-metrics-service 9 | app.kubernetes.io/component: kube-rbac-proxy 10 | app.kubernetes.io/created-by: onepassword-connect-operator 11 | app.kubernetes.io/part-of: onepassword-connect-operator 12 | app.kubernetes.io/managed-by: kustomize 13 | name: onepassword-connect-operator-metrics-service 14 | namespace: system 15 | spec: 16 | ports: 17 | - name: https 18 | port: 8443 19 | protocol: TCP 20 | targetPort: https 21 | selector: 22 | name: onepassword-connect-operator 23 | control-plane: onepassword-connect-operator 24 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /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 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: onepassword-connect-operator 10 | app.kubernetes.io/part-of: onepassword-connect-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: onepassword-connect-operator 9 | app.kubernetes.io/part-of: onepassword-connect-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: onepassword-connect-operator 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/onepassworditem_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit onepassworditems. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: onepassworditem-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: onepassword-connect-operator 10 | app.kubernetes.io/part-of: onepassword-connect-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: onepassworditem-editor-role 13 | rules: 14 | - apiGroups: 15 | - onepassword.com 16 | resources: 17 | - onepassworditems 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - onepassword.com 28 | resources: 29 | - onepassworditems/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/onepassworditem_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view onepassworditems. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: onepassworditem-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: onepassword-connect-operator 10 | app.kubernetes.io/part-of: onepassword-connect-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: onepassworditem-viewer-role 13 | rules: 14 | - apiGroups: 15 | - onepassword.com 16 | resources: 17 | - onepassworditems 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - onepassword.com 24 | resources: 25 | - onepassworditems/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - endpoints 12 | - events 13 | - namespaces 14 | - persistentvolumeclaims 15 | - pods 16 | - secrets 17 | - services 18 | - services/finalizers 19 | verbs: 20 | - create 21 | - delete 22 | - get 23 | - list 24 | - patch 25 | - update 26 | - watch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - pods 31 | verbs: 32 | - get 33 | - apiGroups: 34 | - apps 35 | resources: 36 | - daemonsets 37 | - deployments 38 | - replicasets 39 | - statefulsets 40 | verbs: 41 | - create 42 | - delete 43 | - get 44 | - list 45 | - patch 46 | - update 47 | - watch 48 | - apiGroups: 49 | - apps 50 | resources: 51 | - deployments 52 | verbs: 53 | - create 54 | - delete 55 | - get 56 | - list 57 | - patch 58 | - update 59 | - watch 60 | - apiGroups: 61 | - apps 62 | resources: 63 | - deployments 64 | - replicasets 65 | verbs: 66 | - get 67 | - apiGroups: 68 | - apps 69 | resources: 70 | - deployments/finalizers 71 | verbs: 72 | - update 73 | - apiGroups: 74 | - apps 75 | resourceNames: 76 | - onepassword-connect-operator 77 | resources: 78 | - deployments/finalizers 79 | verbs: 80 | - update 81 | - apiGroups: 82 | - apps 83 | resources: 84 | - deployments/status 85 | verbs: 86 | - get 87 | - patch 88 | - update 89 | - apiGroups: 90 | - coordination.k8s.io 91 | resources: 92 | - leases 93 | verbs: 94 | - create 95 | - get 96 | - list 97 | - update 98 | - apiGroups: 99 | - monitoring.coreos.com 100 | resources: 101 | - servicemonitors 102 | verbs: 103 | - create 104 | - get 105 | - apiGroups: 106 | - onepassword.com 107 | resources: 108 | - '*' 109 | verbs: 110 | - create 111 | - delete 112 | - get 113 | - list 114 | - patch 115 | - update 116 | - watch 117 | - apiGroups: 118 | - onepassword.com 119 | resources: 120 | - onepassworditems 121 | verbs: 122 | - create 123 | - delete 124 | - get 125 | - list 126 | - patch 127 | - update 128 | - watch 129 | - apiGroups: 130 | - onepassword.com 131 | resources: 132 | - onepassworditems/finalizers 133 | verbs: 134 | - update 135 | - apiGroups: 136 | - onepassword.com 137 | resources: 138 | - onepassworditems/status 139 | verbs: 140 | - get 141 | - patch 142 | - update 143 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: onepassword-connect-operator 9 | app.kubernetes.io/part-of: onepassword-connect-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: onepassword-connect-operator 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: onepassword-connect-operator 9 | app.kubernetes.io/part-of: onepassword-connect-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: onepassword-connect-operator 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - onepassword_v1_onepassworditem.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /config/samples/onepassword_v1_onepassworditem.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: onepassword.com/v1 2 | kind: OnePasswordItem 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: onepassworditem 6 | app.kubernetes.io/instance: onepassworditem-sample 7 | app.kubernetes.io/part-of: onepassword-connect-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: onepassword-connect-operator 10 | name: onepassworditem-sample 11 | spec: 12 | itemPath: "vaults//items/" 13 | -------------------------------------------------------------------------------- /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.33.0 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.33.0 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.33.0 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.33.0 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.33.0 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.33.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/1Password/onepassword-operator 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.5 6 | 7 | require ( 8 | github.com/1Password/connect-sdk-go v1.5.3 9 | github.com/onsi/ginkgo/v2 v2.14.0 10 | github.com/onsi/gomega v1.30.0 11 | github.com/stretchr/testify v1.9.0 12 | k8s.io/api v0.29.3 13 | k8s.io/apimachinery v0.29.3 14 | k8s.io/client-go v0.29.3 15 | k8s.io/kubectl v0.29.0 16 | sigs.k8s.io/controller-runtime v0.17.2 17 | ) 18 | 19 | require ( 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 24 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 25 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 26 | github.com/fsnotify/fsnotify v1.7.0 // indirect 27 | github.com/go-logr/logr v1.4.1 // indirect 28 | github.com/go-logr/zapr v1.3.0 // indirect 29 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 30 | github.com/go-openapi/jsonreference v0.21.0 // indirect 31 | github.com/go-openapi/swag v0.23.0 // indirect 32 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 33 | github.com/gogo/protobuf v1.3.2 // indirect 34 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 35 | github.com/golang/protobuf v1.5.4 // indirect 36 | github.com/google/gnostic-models v0.6.8 // indirect 37 | github.com/google/go-cmp v0.6.0 // indirect 38 | github.com/google/gofuzz v1.2.0 // indirect 39 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect 40 | github.com/google/uuid v1.6.0 // indirect 41 | github.com/imdario/mergo v0.3.16 // indirect 42 | github.com/josharian/intern v1.0.0 // indirect 43 | github.com/json-iterator/go v1.1.12 // indirect 44 | github.com/mailru/easyjson v0.7.7 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 48 | github.com/opentracing/opentracing-go v1.2.0 // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/pmezard/go-difflib v1.0.0 // indirect 51 | github.com/prometheus/client_golang v1.19.0 // indirect 52 | github.com/prometheus/client_model v0.6.0 // indirect 53 | github.com/prometheus/common v0.51.1 // indirect 54 | github.com/prometheus/procfs v0.13.0 // indirect 55 | github.com/spf13/pflag v1.0.5 // indirect 56 | github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect 57 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 58 | go.uber.org/atomic v1.11.0 // indirect 59 | go.uber.org/multierr v1.11.0 // indirect 60 | go.uber.org/zap v1.27.0 // indirect 61 | golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 // indirect 62 | golang.org/x/net v0.22.0 // indirect 63 | golang.org/x/oauth2 v0.18.0 // indirect 64 | golang.org/x/sys v0.18.0 // indirect 65 | golang.org/x/term v0.18.0 // indirect 66 | golang.org/x/text v0.14.0 // indirect 67 | golang.org/x/time v0.5.0 // indirect 68 | golang.org/x/tools v0.19.0 // indirect 69 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 70 | google.golang.org/appengine v1.6.8 // indirect 71 | google.golang.org/protobuf v1.33.0 // indirect 72 | gopkg.in/inf.v0 v0.9.1 // indirect 73 | gopkg.in/yaml.v2 v2.4.0 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | k8s.io/apiextensions-apiserver v0.29.3 // indirect 76 | k8s.io/component-base v0.29.3 // indirect 77 | k8s.io/klog/v2 v2.120.1 // indirect 78 | k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 // indirect 79 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect 80 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 81 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 82 | sigs.k8s.io/yaml v1.4.0 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020-2024 1Password 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ -------------------------------------------------------------------------------- /internal/controller/deployment_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020-2024 1Password 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package controller 26 | 27 | import ( 28 | "context" 29 | "fmt" 30 | "regexp" 31 | 32 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 33 | 34 | "github.com/1Password/connect-sdk-go/connect" 35 | 36 | kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" 37 | "github.com/1Password/onepassword-operator/pkg/logs" 38 | op "github.com/1Password/onepassword-operator/pkg/onepassword" 39 | "github.com/1Password/onepassword-operator/pkg/utils" 40 | 41 | appsv1 "k8s.io/api/apps/v1" 42 | corev1 "k8s.io/api/core/v1" 43 | "k8s.io/apimachinery/pkg/api/errors" 44 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 45 | "k8s.io/apimachinery/pkg/runtime" 46 | ctrl "sigs.k8s.io/controller-runtime" 47 | "sigs.k8s.io/controller-runtime/pkg/client" 48 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 49 | logf "sigs.k8s.io/controller-runtime/pkg/log" 50 | ) 51 | 52 | var logDeployment = logf.Log.WithName("controller_deployment") 53 | 54 | // DeploymentReconciler reconciles a Deployment object 55 | type DeploymentReconciler struct { 56 | client.Client 57 | Scheme *runtime.Scheme 58 | OpConnectClient connect.Client 59 | OpAnnotationRegExp *regexp.Regexp 60 | } 61 | 62 | //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete 63 | //+kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch 64 | //+kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update 65 | 66 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 67 | // move the current state of the cluster closer to the desired state. 68 | // TODO(user): Modify the Reconcile function to compare the state specified by 69 | // the OnePasswordItem object against the actual cluster state, and then 70 | // perform operations to make the cluster state reflect the state specified by 71 | // the user. 72 | // 73 | // For more details, check Reconcile and its Result here: 74 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile 75 | func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 76 | reqLogger := logDeployment.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name) 77 | reqLogger.V(logs.DebugLevel).Info("Reconciling Deployment") 78 | 79 | deployment := &appsv1.Deployment{} 80 | err := r.Get(context.Background(), req.NamespacedName, deployment) 81 | if err != nil { 82 | if errors.IsNotFound(err) { 83 | return reconcile.Result{}, nil 84 | } 85 | return ctrl.Result{}, err 86 | } 87 | 88 | annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.OpAnnotationRegExp) 89 | if !annotationsFound { 90 | reqLogger.V(logs.DebugLevel).Info("No 1Password Annotations found") 91 | return ctrl.Result{}, nil 92 | } 93 | 94 | //If the deployment is not being deleted 95 | if deployment.ObjectMeta.DeletionTimestamp.IsZero() { 96 | // Adds a finalizer to the deployment if one does not exist. 97 | // This is so we can handle cleanup of associated secrets properly 98 | if !utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) { 99 | deployment.ObjectMeta.Finalizers = append(deployment.ObjectMeta.Finalizers, finalizer) 100 | if err = r.Update(context.Background(), deployment); err != nil { 101 | return reconcile.Result{}, err 102 | } 103 | } 104 | // Handles creation or updating secrets for deployment if needed 105 | if err = r.handleApplyingDeployment(deployment, deployment.Namespace, annotations, req); err != nil { 106 | return ctrl.Result{}, err 107 | } 108 | return ctrl.Result{}, nil 109 | } 110 | // The deployment has been marked for deletion. If the one password 111 | // finalizer is found there are cleanup tasks to perform 112 | if utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) { 113 | 114 | secretName := annotations[op.NameAnnotation] 115 | if err = r.cleanupKubernetesSecretForDeployment(secretName, deployment); err != nil { 116 | return ctrl.Result{}, err 117 | } 118 | 119 | // Remove the finalizer from the deployment so deletion of deployment can be completed 120 | if err = r.removeOnePasswordFinalizerFromDeployment(deployment); err != nil { 121 | return reconcile.Result{}, err 122 | } 123 | } 124 | return ctrl.Result{}, nil 125 | } 126 | 127 | // SetupWithManager sets up the controller with the Manager. 128 | func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { 129 | return ctrl.NewControllerManagedBy(mgr). 130 | For(&appsv1.Deployment{}). 131 | Complete(r) 132 | } 133 | 134 | func (r *DeploymentReconciler) cleanupKubernetesSecretForDeployment(secretName string, deletedDeployment *appsv1.Deployment) error { 135 | kubernetesSecret := &corev1.Secret{} 136 | kubernetesSecret.ObjectMeta.Name = secretName 137 | kubernetesSecret.ObjectMeta.Namespace = deletedDeployment.Namespace 138 | 139 | if len(secretName) == 0 { 140 | return nil 141 | } 142 | updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret} 143 | 144 | multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | // Only delete the associated kubernetes secret if it is not being used by other deployments 150 | if !multipleDeploymentsUsingSecret { 151 | if err = r.Delete(context.Background(), kubernetesSecret); err != nil { 152 | if !errors.IsNotFound(err) { 153 | return err 154 | } 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | func (r *DeploymentReconciler) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) { 161 | deployments := &appsv1.DeploymentList{} 162 | opts := []client.ListOption{ 163 | client.InNamespace(deletedDeployment.Namespace), 164 | } 165 | 166 | err := r.List(context.Background(), deployments, opts...) 167 | if err != nil { 168 | logDeployment.Error(err, "Failed to list kubernetes deployments") 169 | return false, err 170 | } 171 | 172 | for i := 0; i < len(deployments.Items); i++ { 173 | if deployments.Items[i].Name != deletedDeployment.Name { 174 | if op.IsDeploymentUsingSecrets(&deployments.Items[i], updatedSecrets) { 175 | return true, nil 176 | } 177 | } 178 | } 179 | return false, nil 180 | } 181 | 182 | func (r *DeploymentReconciler) removeOnePasswordFinalizerFromDeployment(deployment *appsv1.Deployment) error { 183 | deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer) 184 | return r.Update(context.Background(), deployment) 185 | } 186 | 187 | func (r *DeploymentReconciler) handleApplyingDeployment(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error { 188 | reqLog := logDeployment.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) 189 | 190 | secretName := annotations[op.NameAnnotation] 191 | secretLabels := map[string]string(nil) 192 | secretType := string(corev1.SecretTypeOpaque) 193 | 194 | if len(secretName) == 0 { 195 | reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.") 196 | return nil 197 | } 198 | 199 | item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, annotations[op.ItemPathAnnotation]) 200 | if err != nil { 201 | return fmt.Errorf("Failed to retrieve item: %v", err) 202 | } 203 | 204 | // Create owner reference. 205 | gvk, err := apiutil.GVKForObject(deployment, r.Scheme) 206 | if err != nil { 207 | return fmt.Errorf("could not to retrieve group version kind: %v", err) 208 | } 209 | ownerRef := &metav1.OwnerReference{ 210 | APIVersion: gvk.GroupVersion().String(), 211 | Kind: gvk.Kind, 212 | Name: deployment.GetName(), 213 | UID: deployment.GetUID(), 214 | } 215 | 216 | return kubeSecrets.CreateKubernetesSecretFromItem(r.Client, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef) 217 | } 218 | -------------------------------------------------------------------------------- /internal/controller/onepassworditem_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020-2024 1Password 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package controller 26 | 27 | import ( 28 | "context" 29 | "fmt" 30 | 31 | "github.com/1Password/connect-sdk-go/connect" 32 | 33 | onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" 34 | kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" 35 | "github.com/1Password/onepassword-operator/pkg/logs" 36 | op "github.com/1Password/onepassword-operator/pkg/onepassword" 37 | "github.com/1Password/onepassword-operator/pkg/utils" 38 | 39 | corev1 "k8s.io/api/core/v1" 40 | "k8s.io/apimachinery/pkg/api/errors" 41 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 42 | "k8s.io/apimachinery/pkg/runtime" 43 | ctrl "sigs.k8s.io/controller-runtime" 44 | "sigs.k8s.io/controller-runtime/pkg/client" 45 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 46 | logf "sigs.k8s.io/controller-runtime/pkg/log" 47 | ) 48 | 49 | var logOnePasswordItem = logf.Log.WithName("controller_onepassworditem") 50 | var finalizer = "onepassword.com/finalizer.secret" 51 | 52 | // OnePasswordItemReconciler reconciles a OnePasswordItem object 53 | type OnePasswordItemReconciler struct { 54 | client.Client 55 | Scheme *runtime.Scheme 56 | OpConnectClient connect.Client 57 | } 58 | 59 | //+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete 60 | //+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/status,verbs=get;update;patch 61 | //+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/finalizers,verbs=update 62 | 63 | //+kubebuilder:rbac:groups="",resources=pods,verbs=get 64 | //+kubebuilder:rbac:groups="",resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets;namespaces,verbs=get;list;watch;create;update;patch;delete 65 | //+kubebuilder:rbac:groups=apps,resources=daemonsets;deployments;replicasets;statefulsets,verbs=get;list;watch;create;update;patch;delete 66 | //+kubebuilder:rbac:groups=apps,resources=replicasets;deployments,verbs=get 67 | //+kubebuilder:rbac:groups=apps,resourceNames=onepassword-connect-operator,resources=deployments/finalizers,verbs=update 68 | //+kubebuilder:rbac:groups=onepassword.com,resources=*,verbs=get;list;watch;create;update;patch;delete 69 | //+kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create 70 | //+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update 71 | 72 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 73 | // move the current state of the cluster closer to the desired state. 74 | // TODO(user): Modify the Reconcile function to compare the state specified by 75 | // the OnePasswordItem object against the actual cluster state, and then 76 | // perform operations to make the cluster state reflect the state specified by 77 | // the user. 78 | // 79 | // For more details, check Reconcile and its Result here: 80 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile 81 | func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 82 | reqLogger := logOnePasswordItem.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name) 83 | reqLogger.V(logs.DebugLevel).Info("Reconciling OnePasswordItem") 84 | 85 | onepassworditem := &onepasswordv1.OnePasswordItem{} 86 | err := r.Get(context.Background(), req.NamespacedName, onepassworditem) 87 | if err != nil { 88 | if errors.IsNotFound(err) { 89 | return ctrl.Result{}, nil 90 | } 91 | return ctrl.Result{}, err 92 | } 93 | 94 | // If the deployment is not being deleted 95 | if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() { 96 | // Adds a finalizer to the deployment if one does not exist. 97 | // This is so we can handle cleanup of associated secrets properly 98 | if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { 99 | onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer) 100 | if err = r.Update(context.Background(), onepassworditem); err != nil { 101 | return ctrl.Result{}, err 102 | } 103 | } 104 | 105 | // Handles creation or updating secrets for deployment if needed 106 | err = r.handleOnePasswordItem(onepassworditem, req) 107 | if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil { 108 | return ctrl.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr) 109 | } 110 | return ctrl.Result{}, err 111 | } 112 | // If one password finalizer exists then we must cleanup associated secrets 113 | if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { 114 | 115 | // Delete associated kubernetes secret 116 | if err = r.cleanupKubernetesSecret(onepassworditem); err != nil { 117 | return ctrl.Result{}, err 118 | } 119 | 120 | // Remove finalizer now that cleanup is complete 121 | if err = r.removeFinalizer(onepassworditem); err != nil { 122 | return ctrl.Result{}, err 123 | } 124 | } 125 | return ctrl.Result{}, nil 126 | } 127 | 128 | // SetupWithManager sets up the controller with the Manager. 129 | func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error { 130 | return ctrl.NewControllerManagedBy(mgr). 131 | For(&onepasswordv1.OnePasswordItem{}). 132 | Complete(r) 133 | } 134 | 135 | func (r *OnePasswordItemReconciler) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error { 136 | onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer) 137 | if err := r.Update(context.Background(), onePasswordItem); err != nil { 138 | return err 139 | } 140 | return nil 141 | } 142 | 143 | func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error { 144 | kubernetesSecret := &corev1.Secret{} 145 | kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name 146 | kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace 147 | 148 | if err := r.Delete(context.Background(), kubernetesSecret); err != nil { 149 | if !errors.IsNotFound(err) { 150 | return err 151 | } 152 | } 153 | return nil 154 | } 155 | 156 | func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(opSecret *onepasswordv1.OnePasswordItem) error { 157 | opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer) 158 | return r.Update(context.Background(), opSecret) 159 | } 160 | 161 | func (r *OnePasswordItemReconciler) handleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, req ctrl.Request) error { 162 | secretName := resource.GetName() 163 | labels := resource.Labels 164 | secretType := resource.Type 165 | autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation] 166 | 167 | item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, resource.Spec.ItemPath) 168 | if err != nil { 169 | return fmt.Errorf("Failed to retrieve item: %v", err) 170 | } 171 | 172 | // Create owner reference. 173 | gvk, err := apiutil.GVKForObject(resource, r.Scheme) 174 | if err != nil { 175 | return fmt.Errorf("could not to retrieve group version kind: %v", err) 176 | } 177 | ownerRef := &metav1.OwnerReference{ 178 | APIVersion: gvk.GroupVersion().String(), 179 | Kind: gvk.Kind, 180 | Name: resource.GetName(), 181 | UID: resource.GetUID(), 182 | } 183 | 184 | return kubeSecrets.CreateKubernetesSecretFromItem(r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef) 185 | } 186 | 187 | func (r *OnePasswordItemReconciler) updateStatus(resource *onepasswordv1.OnePasswordItem, err error) error { 188 | existingCondition := findCondition(resource.Status.Conditions, onepasswordv1.OnePasswordItemReady) 189 | updatedCondition := existingCondition 190 | if err != nil { 191 | updatedCondition.Message = err.Error() 192 | updatedCondition.Status = metav1.ConditionFalse 193 | } else { 194 | updatedCondition.Message = "" 195 | updatedCondition.Status = metav1.ConditionTrue 196 | } 197 | 198 | if existingCondition.Status != updatedCondition.Status { 199 | updatedCondition.LastTransitionTime = metav1.Now() 200 | } 201 | 202 | resource.Status.Conditions = []onepasswordv1.OnePasswordItemCondition{updatedCondition} 203 | return r.Status().Update(context.Background(), resource) 204 | } 205 | 206 | func findCondition(conditions []onepasswordv1.OnePasswordItemCondition, t onepasswordv1.OnePasswordItemConditionType) onepasswordv1.OnePasswordItemCondition { 207 | for _, c := range conditions { 208 | if c.Type == t { 209 | return c 210 | } 211 | } 212 | return onepasswordv1.OnePasswordItemCondition{ 213 | Type: t, 214 | Status: metav1.ConditionUnknown, 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /internal/controller/onepassworditem_controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/1Password/connect-sdk-go/onepassword" 7 | "github.com/1Password/onepassword-operator/pkg/mocks" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | 12 | v1 "k8s.io/api/core/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/types" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 17 | 18 | onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" 19 | ) 20 | 21 | const ( 22 | firstHost = "http://localhost:8080" 23 | awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 24 | iceCream = "freezing blue 20%" 25 | ) 26 | 27 | var _ = Describe("OnePasswordItem controller", func() { 28 | BeforeEach(func() { 29 | // failed test runs that don't clean up leave resources behind. 30 | err := k8sClient.DeleteAllOf(context.Background(), &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace)) 31 | Expect(err).ToNot(HaveOccurred()) 32 | err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace)) 33 | Expect(err).ToNot(HaveOccurred()) 34 | 35 | mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { 36 | item := onepassword.Item{} 37 | item.Fields = []*onepassword.ItemField{} 38 | for k, v := range item1.Data { 39 | item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) 40 | } 41 | item.Version = item1.Version 42 | item.Vault.ID = vaultUUID 43 | item.ID = uuid 44 | return &item, nil 45 | } 46 | }) 47 | 48 | Context("Happy path", func() { 49 | It("Should handle 1Password Item and secret correctly", func() { 50 | ctx := context.Background() 51 | spec := onepasswordv1.OnePasswordItemSpec{ 52 | ItemPath: item1.Path, 53 | } 54 | 55 | key := types.NamespacedName{ 56 | Name: "sample-item", 57 | Namespace: namespace, 58 | } 59 | 60 | toCreate := &onepasswordv1.OnePasswordItem{ 61 | ObjectMeta: metav1.ObjectMeta{ 62 | Name: key.Name, 63 | Namespace: key.Namespace, 64 | }, 65 | Spec: spec, 66 | } 67 | 68 | By("Creating a new OnePasswordItem successfully") 69 | Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) 70 | 71 | created := &onepasswordv1.OnePasswordItem{} 72 | Eventually(func() bool { 73 | err := k8sClient.Get(ctx, key, created) 74 | if err != nil { 75 | return false 76 | } 77 | return true 78 | }, timeout, interval).Should(BeTrue()) 79 | 80 | By("Creating the K8s secret successfully") 81 | createdSecret := &v1.Secret{} 82 | Eventually(func() bool { 83 | err := k8sClient.Get(ctx, key, createdSecret) 84 | if err != nil { 85 | return false 86 | } 87 | return true 88 | }, timeout, interval).Should(BeTrue()) 89 | Expect(createdSecret.Data).Should(Equal(item1.SecretData)) 90 | 91 | By("Updating existing secret successfully") 92 | newData := map[string]string{ 93 | "username": "newUser1234", 94 | "password": "##newPassword##", 95 | "extraField": "dev", 96 | } 97 | newDataByte := map[string][]byte{ 98 | "username": []byte("newUser1234"), 99 | "password": []byte("##newPassword##"), 100 | "extraField": []byte("dev"), 101 | } 102 | mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { 103 | item := onepassword.Item{} 104 | item.Fields = []*onepassword.ItemField{} 105 | for k, v := range newData { 106 | item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) 107 | } 108 | item.Version = item1.Version + 1 109 | item.Vault.ID = vaultUUID 110 | item.ID = uuid 111 | return &item, nil 112 | } 113 | _, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) 114 | Expect(err).ToNot(HaveOccurred()) 115 | 116 | updatedSecret := &v1.Secret{} 117 | Eventually(func() bool { 118 | err := k8sClient.Get(ctx, key, updatedSecret) 119 | if err != nil { 120 | return false 121 | } 122 | return true 123 | }, timeout, interval).Should(BeTrue()) 124 | Expect(updatedSecret.Data).Should(Equal(newDataByte)) 125 | 126 | By("Deleting the OnePasswordItem successfully") 127 | Eventually(func() error { 128 | f := &onepasswordv1.OnePasswordItem{} 129 | err := k8sClient.Get(ctx, key, f) 130 | if err != nil { 131 | return err 132 | } 133 | return k8sClient.Delete(ctx, f) 134 | }, timeout, interval).Should(Succeed()) 135 | 136 | Eventually(func() error { 137 | f := &onepasswordv1.OnePasswordItem{} 138 | return k8sClient.Get(ctx, key, f) 139 | }, timeout, interval).ShouldNot(Succeed()) 140 | 141 | Eventually(func() error { 142 | f := &v1.Secret{} 143 | return k8sClient.Get(ctx, key, f) 144 | }, timeout, interval).ShouldNot(Succeed()) 145 | }) 146 | 147 | It("Should handle 1Password Item with fields and sections that have invalid K8s labels correctly", func() { 148 | ctx := context.Background() 149 | spec := onepasswordv1.OnePasswordItemSpec{ 150 | ItemPath: item1.Path, 151 | } 152 | 153 | key := types.NamespacedName{ 154 | Name: "my-secret-it3m", 155 | Namespace: namespace, 156 | } 157 | 158 | toCreate := &onepasswordv1.OnePasswordItem{ 159 | ObjectMeta: metav1.ObjectMeta{ 160 | Name: key.Name, 161 | Namespace: key.Namespace, 162 | }, 163 | Spec: spec, 164 | } 165 | 166 | testData := map[string]string{ 167 | "username": username, 168 | "password": password, 169 | "first host": firstHost, 170 | "AWS Access Key": awsKey, 171 | "😄 ice-cream type": iceCream, 172 | } 173 | expectedData := map[string][]byte{ 174 | "username": []byte(username), 175 | "password": []byte(password), 176 | "first-host": []byte(firstHost), 177 | "AWS-Access-Key": []byte(awsKey), 178 | "ice-cream-type": []byte(iceCream), 179 | } 180 | 181 | mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { 182 | item := onepassword.Item{} 183 | item.Title = "!my sECReT it3m%" 184 | item.Fields = []*onepassword.ItemField{} 185 | for k, v := range testData { 186 | item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) 187 | } 188 | item.Version = item1.Version + 1 189 | item.Vault.ID = vaultUUID 190 | item.ID = uuid 191 | return &item, nil 192 | } 193 | 194 | By("Creating a new OnePasswordItem successfully") 195 | Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) 196 | 197 | created := &onepasswordv1.OnePasswordItem{} 198 | Eventually(func() bool { 199 | err := k8sClient.Get(ctx, key, created) 200 | if err != nil { 201 | return false 202 | } 203 | return true 204 | }, timeout, interval).Should(BeTrue()) 205 | 206 | By("Creating the K8s secret successfully") 207 | createdSecret := &v1.Secret{} 208 | Eventually(func() bool { 209 | err := k8sClient.Get(ctx, key, createdSecret) 210 | if err != nil { 211 | return false 212 | } 213 | return true 214 | }, timeout, interval).Should(BeTrue()) 215 | Expect(createdSecret.Data).Should(Equal(expectedData)) 216 | 217 | By("Deleting the OnePasswordItem successfully") 218 | Eventually(func() error { 219 | f := &onepasswordv1.OnePasswordItem{} 220 | err := k8sClient.Get(ctx, key, f) 221 | if err != nil { 222 | return err 223 | } 224 | return k8sClient.Delete(ctx, f) 225 | }, timeout, interval).Should(Succeed()) 226 | 227 | Eventually(func() error { 228 | f := &onepasswordv1.OnePasswordItem{} 229 | return k8sClient.Get(ctx, key, f) 230 | }, timeout, interval).ShouldNot(Succeed()) 231 | 232 | Eventually(func() error { 233 | f := &v1.Secret{} 234 | return k8sClient.Get(ctx, key, f) 235 | }, timeout, interval).ShouldNot(Succeed()) 236 | }) 237 | 238 | It("Should not update K8s secret if OnePasswordItem Version or VaultPath has not changed", func() { 239 | ctx := context.Background() 240 | spec := onepasswordv1.OnePasswordItemSpec{ 241 | ItemPath: item1.Path, 242 | } 243 | 244 | key := types.NamespacedName{ 245 | Name: "item-not-updated", 246 | Namespace: namespace, 247 | } 248 | 249 | toCreate := &onepasswordv1.OnePasswordItem{ 250 | ObjectMeta: metav1.ObjectMeta{ 251 | Name: key.Name, 252 | Namespace: key.Namespace, 253 | }, 254 | Spec: spec, 255 | } 256 | 257 | By("Creating a new OnePasswordItem successfully") 258 | Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) 259 | 260 | item := &onepasswordv1.OnePasswordItem{} 261 | Eventually(func() bool { 262 | err := k8sClient.Get(ctx, key, item) 263 | return err == nil 264 | }, timeout, interval).Should(BeTrue()) 265 | 266 | By("Creating the K8s secret successfully") 267 | createdSecret := &v1.Secret{} 268 | Eventually(func() bool { 269 | err := k8sClient.Get(ctx, key, createdSecret) 270 | return err == nil 271 | }, timeout, interval).Should(BeTrue()) 272 | Expect(createdSecret.Data).Should(Equal(item1.SecretData)) 273 | 274 | By("Updating OnePasswordItem type") 275 | Eventually(func() bool { 276 | err1 := k8sClient.Get(ctx, key, item) 277 | if err1 != nil { 278 | return false 279 | } 280 | item.Type = string(v1.SecretTypeOpaque) 281 | err := k8sClient.Update(ctx, item) 282 | return err == nil 283 | }, timeout, interval).Should(BeTrue()) 284 | 285 | By("Reading K8s secret") 286 | secret := &v1.Secret{} 287 | Eventually(func() bool { 288 | err := k8sClient.Get(ctx, key, secret) 289 | return err == nil 290 | }, timeout, interval).Should(BeTrue()) 291 | Expect(secret.Data).Should(Equal(item1.SecretData)) 292 | }) 293 | 294 | It("Should create custom K8s Secret type using OnePasswordItem", func() { 295 | const customType = "CustomType" 296 | ctx := context.Background() 297 | spec := onepasswordv1.OnePasswordItemSpec{ 298 | ItemPath: item1.Path, 299 | } 300 | 301 | key := types.NamespacedName{ 302 | Name: "item-custom-secret-type", 303 | Namespace: namespace, 304 | } 305 | 306 | toCreate := &onepasswordv1.OnePasswordItem{ 307 | ObjectMeta: metav1.ObjectMeta{ 308 | Name: key.Name, 309 | Namespace: key.Namespace, 310 | }, 311 | Spec: spec, 312 | Type: customType, 313 | } 314 | 315 | By("Creating a new OnePasswordItem successfully") 316 | Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) 317 | 318 | By("Reading K8s secret") 319 | secret := &v1.Secret{} 320 | Eventually(func() bool { 321 | err := k8sClient.Get(ctx, key, secret) 322 | if err != nil { 323 | return false 324 | } 325 | return true 326 | }, timeout, interval).Should(BeTrue()) 327 | Expect(secret.Type).Should(Equal(v1.SecretType(customType))) 328 | }) 329 | }) 330 | 331 | Context("Unhappy path", func() { 332 | It("Should throw an error if K8s Secret type is changed", func() { 333 | ctx := context.Background() 334 | spec := onepasswordv1.OnePasswordItemSpec{ 335 | ItemPath: item1.Path, 336 | } 337 | 338 | key := types.NamespacedName{ 339 | Name: "item-changed-secret-type", 340 | Namespace: namespace, 341 | } 342 | 343 | toCreate := &onepasswordv1.OnePasswordItem{ 344 | ObjectMeta: metav1.ObjectMeta{ 345 | Name: key.Name, 346 | Namespace: key.Namespace, 347 | }, 348 | Spec: spec, 349 | } 350 | 351 | By("Creating a new OnePasswordItem successfully") 352 | Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) 353 | 354 | By("Reading K8s secret") 355 | secret := &v1.Secret{} 356 | Eventually(func() bool { 357 | err := k8sClient.Get(ctx, key, secret) 358 | if err != nil { 359 | return false 360 | } 361 | return true 362 | }, timeout, interval).Should(BeTrue()) 363 | 364 | By("Failing to update K8s secret") 365 | Eventually(func() bool { 366 | secret.Type = v1.SecretTypeBasicAuth 367 | err := k8sClient.Update(ctx, secret) 368 | if err != nil { 369 | return false 370 | } 371 | return true 372 | }, timeout, interval).Should(BeFalse()) 373 | }) 374 | 375 | When("OnePasswordItem resource name contains `_`", func() { 376 | It("Should fail creating a OnePasswordItem resource", func() { 377 | ctx := context.Background() 378 | spec := onepasswordv1.OnePasswordItemSpec{ 379 | ItemPath: item1.Path, 380 | } 381 | 382 | key := types.NamespacedName{ 383 | Name: "invalid_name", 384 | Namespace: namespace, 385 | } 386 | 387 | toCreate := &onepasswordv1.OnePasswordItem{ 388 | ObjectMeta: metav1.ObjectMeta{ 389 | Name: key.Name, 390 | Namespace: key.Namespace, 391 | }, 392 | Spec: spec, 393 | } 394 | 395 | By("Creating a new OnePasswordItem") 396 | Expect(k8sClient.Create(ctx, toCreate)).To(HaveOccurred()) 397 | 398 | }) 399 | }) 400 | 401 | When("OnePasswordItem resource name contains capital letters", func() { 402 | It("Should fail creating a OnePasswordItem resource", func() { 403 | ctx := context.Background() 404 | spec := onepasswordv1.OnePasswordItemSpec{ 405 | ItemPath: item1.Path, 406 | } 407 | 408 | key := types.NamespacedName{ 409 | Name: "invalidName", 410 | Namespace: namespace, 411 | } 412 | 413 | toCreate := &onepasswordv1.OnePasswordItem{ 414 | ObjectMeta: metav1.ObjectMeta{ 415 | Name: key.Name, 416 | Namespace: key.Namespace, 417 | }, 418 | Spec: spec, 419 | } 420 | 421 | By("Creating a new OnePasswordItem") 422 | Expect(k8sClient.Create(ctx, toCreate)).To(HaveOccurred()) 423 | }) 424 | }) 425 | }) 426 | }) 427 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020-2024 1Password 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package controller 26 | 27 | import ( 28 | "context" 29 | "path/filepath" 30 | "regexp" 31 | "testing" 32 | "time" 33 | 34 | "github.com/1Password/onepassword-operator/pkg/mocks" 35 | 36 | . "github.com/onsi/ginkgo/v2" 37 | . "github.com/onsi/gomega" 38 | 39 | "k8s.io/client-go/kubernetes/scheme" 40 | "k8s.io/client-go/rest" 41 | ctrl "sigs.k8s.io/controller-runtime" 42 | "sigs.k8s.io/controller-runtime/pkg/client" 43 | "sigs.k8s.io/controller-runtime/pkg/envtest" 44 | logf "sigs.k8s.io/controller-runtime/pkg/log" 45 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 46 | 47 | onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1" 48 | //+kubebuilder:scaffold:imports 49 | ) 50 | 51 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 52 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 53 | 54 | const ( 55 | username = "test-user" 56 | password = "QmHumKc$mUeEem7caHtbaBaJ" 57 | 58 | username2 = "test-user2" 59 | password2 = "4zotzqDqXKasLFT2jzTs" 60 | 61 | annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" 62 | ) 63 | 64 | // Define utility constants for object names and testing timeouts/durations and intervals. 65 | const ( 66 | namespace = "default" 67 | 68 | timeout = time.Second * 10 69 | duration = time.Second * 10 70 | interval = time.Millisecond * 250 71 | ) 72 | 73 | var ( 74 | cfg *rest.Config 75 | k8sClient client.Client 76 | testEnv *envtest.Environment 77 | ctx context.Context 78 | cancel context.CancelFunc 79 | onePasswordItemReconciler *OnePasswordItemReconciler 80 | deploymentReconciler *DeploymentReconciler 81 | 82 | item1 = &TestItem{ 83 | Name: "test-item", 84 | Version: 123, 85 | Path: "vaults/hfnjvi6aymbsnfc2xeeoheizda/items/nwrhuano7bcwddcviubpp4mhfq", 86 | Data: map[string]string{ 87 | "username": username, 88 | "password": password, 89 | }, 90 | SecretData: map[string][]byte{ 91 | "password": []byte(password), 92 | "username": []byte(username), 93 | }, 94 | } 95 | 96 | item2 = &TestItem{ 97 | Name: "test-item2", 98 | Path: "vaults/hfnjvi6aymbsnfc2xeeoheizd2/items/nwrhuano7bcwddcviubpp4mhf2", 99 | Version: 456, 100 | Data: map[string]string{ 101 | "username": username2, 102 | "password": password2, 103 | }, 104 | SecretData: map[string][]byte{ 105 | "password": []byte(password2), 106 | "username": []byte(username2), 107 | }, 108 | } 109 | ) 110 | 111 | type TestItem struct { 112 | Name string 113 | Version int 114 | Path string 115 | Data map[string]string 116 | SecretData map[string][]byte 117 | } 118 | 119 | func TestAPIs(t *testing.T) { 120 | RegisterFailHandler(Fail) 121 | 122 | RunSpecs(t, "Controller Suite") 123 | } 124 | 125 | var _ = BeforeSuite(func() { 126 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 127 | 128 | ctx, cancel = context.WithCancel(context.TODO()) 129 | 130 | By("bootstrapping test environment") 131 | testEnv = &envtest.Environment{ 132 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 133 | ErrorIfCRDPathMissing: true, 134 | } 135 | 136 | var err error 137 | // cfg is defined in this file globally. 138 | cfg, err = testEnv.Start() 139 | Expect(err).NotTo(HaveOccurred()) 140 | Expect(cfg).NotTo(BeNil()) 141 | 142 | err = onepasswordcomv1.AddToScheme(scheme.Scheme) 143 | Expect(err).NotTo(HaveOccurred()) 144 | 145 | //+kubebuilder:scaffold:scheme 146 | 147 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 148 | Expect(err).NotTo(HaveOccurred()) 149 | Expect(k8sClient).NotTo(BeNil()) 150 | 151 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 152 | Scheme: scheme.Scheme, 153 | }) 154 | Expect(err).ToNot(HaveOccurred()) 155 | 156 | opConnectClient := &mocks.TestClient{} 157 | 158 | onePasswordItemReconciler = &OnePasswordItemReconciler{ 159 | Client: k8sManager.GetClient(), 160 | Scheme: k8sManager.GetScheme(), 161 | OpConnectClient: opConnectClient, 162 | } 163 | err = (onePasswordItemReconciler).SetupWithManager(k8sManager) 164 | Expect(err).ToNot(HaveOccurred()) 165 | 166 | r, _ := regexp.Compile(annotationRegExpString) 167 | deploymentReconciler = &DeploymentReconciler{ 168 | Client: k8sManager.GetClient(), 169 | Scheme: k8sManager.GetScheme(), 170 | OpConnectClient: opConnectClient, 171 | OpAnnotationRegExp: r, 172 | } 173 | err = (deploymentReconciler).SetupWithManager(k8sManager) 174 | Expect(err).ToNot(HaveOccurred()) 175 | 176 | go func() { 177 | defer GinkgoRecover() 178 | err = k8sManager.Start(ctx) 179 | Expect(err).ToNot(HaveOccurred(), "failed to run manager") 180 | }() 181 | 182 | }) 183 | 184 | var _ = AfterSuite(func() { 185 | cancel() 186 | By("tearing down the test environment") 187 | err := testEnv.Stop() 188 | Expect(err).NotTo(HaveOccurred()) 189 | }) 190 | -------------------------------------------------------------------------------- /pkg/kubernetessecrets/kubernetes_secrets_builder.go: -------------------------------------------------------------------------------- 1 | package kubernetessecrets 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "regexp" 8 | "strings" 9 | 10 | "reflect" 11 | 12 | errs "errors" 13 | 14 | "github.com/1Password/connect-sdk-go/onepassword" 15 | 16 | "github.com/1Password/onepassword-operator/pkg/utils" 17 | corev1 "k8s.io/api/core/v1" 18 | "k8s.io/apimachinery/pkg/api/errors" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | "k8s.io/apimachinery/pkg/types" 21 | kubeValidate "k8s.io/apimachinery/pkg/util/validation" 22 | 23 | kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" 24 | logf "sigs.k8s.io/controller-runtime/pkg/log" 25 | ) 26 | 27 | const OnepasswordPrefix = "operator.1password.io" 28 | const NameAnnotation = OnepasswordPrefix + "/item-name" 29 | const VersionAnnotation = OnepasswordPrefix + "/item-version" 30 | const ItemPathAnnotation = OnepasswordPrefix + "/item-path" 31 | const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" 32 | 33 | var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable") 34 | 35 | var log = logf.Log 36 | 37 | func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error { 38 | itemVersion := fmt.Sprint(item.Version) 39 | secretAnnotations := map[string]string{ 40 | VersionAnnotation: itemVersion, 41 | ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID), 42 | } 43 | 44 | if autoRestart != "" { 45 | _, err := utils.StringToBool(autoRestart) 46 | if err != nil { 47 | return fmt.Errorf("Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) 48 | } 49 | secretAnnotations[RestartDeploymentsAnnotation] = autoRestart 50 | } 51 | 52 | // "Opaque" and "" secret types are treated the same by Kubernetes. 53 | secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item, ownerRef) 54 | 55 | currentSecret := &corev1.Secret{} 56 | err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) 57 | if err != nil && errors.IsNotFound(err) { 58 | log.Info(fmt.Sprintf("Creating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) 59 | return kubeClient.Create(context.Background(), secret) 60 | } else if err != nil { 61 | return err 62 | } 63 | 64 | // Check if the secret types are being changed on the update. 65 | // Avoid Opaque and "" are treated as different on check. 66 | wantSecretType := secretType 67 | if wantSecretType == "" { 68 | wantSecretType = string(corev1.SecretTypeOpaque) 69 | } 70 | currentSecretType := string(currentSecret.Type) 71 | if currentSecretType == "" { 72 | currentSecretType = string(corev1.SecretTypeOpaque) 73 | } 74 | if currentSecretType != wantSecretType { 75 | return ErrCannotUpdateSecretType 76 | } 77 | 78 | currentAnnotations := currentSecret.Annotations 79 | currentLabels := currentSecret.Labels 80 | if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) { 81 | log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) 82 | currentSecret.ObjectMeta.Annotations = secretAnnotations 83 | currentSecret.ObjectMeta.Labels = labels 84 | currentSecret.Data = secret.Data 85 | if err := kubeClient.Update(context.Background(), currentSecret); err != nil { 86 | return fmt.Errorf("Kubernetes secret update failed: %w", err) 87 | } 88 | return nil 89 | } 90 | 91 | log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation])) 92 | return nil 93 | } 94 | 95 | func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret { 96 | var ownerRefs []metav1.OwnerReference 97 | if ownerRef != nil { 98 | ownerRefs = []metav1.OwnerReference{*ownerRef} 99 | } 100 | 101 | return &corev1.Secret{ 102 | ObjectMeta: metav1.ObjectMeta{ 103 | Name: formatSecretName(name), 104 | Namespace: namespace, 105 | Annotations: annotations, 106 | Labels: labels, 107 | OwnerReferences: ownerRefs, 108 | }, 109 | Data: BuildKubernetesSecretData(item.Fields, item.Files), 110 | Type: corev1.SecretType(secretType), 111 | } 112 | } 113 | 114 | func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte { 115 | secretData := map[string][]byte{} 116 | for i := 0; i < len(fields); i++ { 117 | if fields[i].Value != "" { 118 | key := formatSecretDataName(fields[i].Label) 119 | secretData[key] = []byte(fields[i].Value) 120 | } 121 | } 122 | 123 | // populate unpopulated fields from files 124 | for _, file := range files { 125 | content, err := file.Content() 126 | if err != nil { 127 | log.Error(err, "Could not load contents of file %s", file.Name) 128 | continue 129 | } 130 | if content != nil { 131 | key := file.Name 132 | if secretData[key] == nil { 133 | secretData[key] = content 134 | } else { 135 | log.Info(fmt.Sprintf("File '%s' ignored because of a field with the same name", file.Name)) 136 | } 137 | } 138 | } 139 | return secretData 140 | } 141 | 142 | // formatSecretName rewrites a value to be a valid Secret name. 143 | // 144 | // The Secret meta.name and data keys must be valid DNS subdomain names 145 | // (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) 146 | func formatSecretName(value string) string { 147 | if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 { 148 | return value 149 | } 150 | return createValidSecretName(value) 151 | } 152 | 153 | // formatSecretDataName rewrites a value to be a valid Secret data key. 154 | // 155 | // The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.` 156 | // (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) 157 | func formatSecretDataName(value string) string { 158 | if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 { 159 | return value 160 | } 161 | return createValidSecretDataName(value) 162 | } 163 | 164 | var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+") 165 | 166 | func createValidSecretName(value string) string { 167 | result := strings.ToLower(value) 168 | result = invalidDNS1123Chars.ReplaceAllString(result, "-") 169 | 170 | if len(result) > kubeValidate.DNS1123SubdomainMaxLength { 171 | result = result[0:kubeValidate.DNS1123SubdomainMaxLength] 172 | } 173 | 174 | // first and last character MUST be alphanumeric 175 | return strings.Trim(result, "-.") 176 | } 177 | 178 | var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+") 179 | var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)") 180 | 181 | func createValidSecretDataName(value string) string { 182 | result := invalidStartEndChars.ReplaceAllString(value, "") 183 | result = invalidDataChars.ReplaceAllString(result, "-") 184 | 185 | if len(result) > kubeValidate.DNS1123SubdomainMaxLength { 186 | result = result[0:kubeValidate.DNS1123SubdomainMaxLength] 187 | } 188 | 189 | return result 190 | } 191 | -------------------------------------------------------------------------------- /pkg/kubernetessecrets/kubernetes_secrets_builder_test.go: -------------------------------------------------------------------------------- 1 | package kubernetessecrets 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/1Password/connect-sdk-go/onepassword" 10 | 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/types" 14 | kubeValidate "k8s.io/apimachinery/pkg/util/validation" 15 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 16 | ) 17 | 18 | const restartDeploymentAnnotation = "false" 19 | 20 | func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { 21 | secretName := "test-secret-name" 22 | namespace := "test" 23 | 24 | item := onepassword.Item{} 25 | item.Fields = generateFields(5) 26 | item.Version = 123 27 | item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" 28 | item.ID = "h46bb3jddvay7nxopfhvlwg35q" 29 | 30 | kubeClient := fake.NewClientBuilder().Build() 31 | secretLabels := map[string]string{} 32 | secretType := "" 33 | 34 | err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) 35 | if err != nil { 36 | t.Errorf("Unexpected error: %v", err) 37 | } 38 | createdSecret := &corev1.Secret{} 39 | err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) 40 | 41 | if err != nil { 42 | t.Errorf("Secret was not created: %v", err) 43 | } 44 | compareFields(item.Fields, createdSecret.Data, t) 45 | compareAnnotationsToItem(createdSecret.Annotations, item, t) 46 | } 47 | 48 | func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) { 49 | secretName := "test-secret-name" 50 | namespace := "test" 51 | 52 | item := onepassword.Item{} 53 | item.Fields = generateFields(5) 54 | item.Version = 123 55 | item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" 56 | item.ID = "h46bb3jddvay7nxopfhvlwg35q" 57 | 58 | kubeClient := fake.NewClientBuilder().Build() 59 | secretLabels := map[string]string{} 60 | secretType := "" 61 | 62 | ownerRef := &metav1.OwnerReference{ 63 | Kind: "Deployment", 64 | APIVersion: "apps/v1", 65 | Name: "test-deployment", 66 | UID: types.UID("test-uid"), 67 | } 68 | err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, ownerRef) 69 | if err != nil { 70 | t.Errorf("Unexpected error: %v", err) 71 | } 72 | createdSecret := &corev1.Secret{} 73 | err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) 74 | 75 | // Check owner references. 76 | gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences 77 | if len(gotOwnerRefs) != 1 { 78 | t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs)) 79 | } 80 | 81 | expOwnerRef := metav1.OwnerReference{ 82 | Kind: "Deployment", 83 | APIVersion: "apps/v1", 84 | Name: "test-deployment", 85 | UID: types.UID("test-uid"), 86 | } 87 | gotOwnerRef := gotOwnerRefs[0] 88 | if gotOwnerRef != expOwnerRef { 89 | t.Errorf("Expected owner reference value: %v but got: %v", expOwnerRef, gotOwnerRef) 90 | } 91 | } 92 | 93 | func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { 94 | secretName := "test-secret-update" 95 | namespace := "test" 96 | 97 | item := onepassword.Item{} 98 | item.Fields = generateFields(5) 99 | item.Version = 123 100 | item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" 101 | item.ID = "h46bb3jddvay7nxopfhvlwg35q" 102 | 103 | kubeClient := fake.NewClientBuilder().Build() 104 | secretLabels := map[string]string{} 105 | secretType := "" 106 | 107 | err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) 108 | 109 | if err != nil { 110 | t.Errorf("Unexpected error: %v", err) 111 | } 112 | 113 | // Updating kubernetes secret with new item 114 | newItem := onepassword.Item{} 115 | newItem.Fields = generateFields(6) 116 | newItem.Version = 456 117 | newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" 118 | newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" 119 | err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil) 120 | if err != nil { 121 | t.Errorf("Unexpected error: %v", err) 122 | } 123 | updatedSecret := &corev1.Secret{} 124 | err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, updatedSecret) 125 | 126 | if err != nil { 127 | t.Errorf("Secret was not found: %v", err) 128 | } 129 | compareFields(newItem.Fields, updatedSecret.Data, t) 130 | compareAnnotationsToItem(updatedSecret.Annotations, newItem, t) 131 | } 132 | func TestBuildKubernetesSecretData(t *testing.T) { 133 | fields := generateFields(5) 134 | 135 | secretData := BuildKubernetesSecretData(fields, nil) 136 | if len(secretData) != len(fields) { 137 | t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData)) 138 | } 139 | compareFields(fields, secretData, t) 140 | } 141 | 142 | func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { 143 | annotationKey := "annotationKey" 144 | annotationValue := "annotationValue" 145 | name := "someName" 146 | namespace := "someNamespace" 147 | annotations := map[string]string{ 148 | annotationKey: annotationValue, 149 | } 150 | item := onepassword.Item{} 151 | item.Fields = generateFields(5) 152 | labels := map[string]string{} 153 | secretType := "" 154 | 155 | kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil) 156 | if kubeSecret.Name != strings.ToLower(name) { 157 | t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) 158 | } 159 | if kubeSecret.Namespace != namespace { 160 | t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace) 161 | } 162 | if kubeSecret.Annotations[annotationKey] != annotations[annotationKey] { 163 | t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace) 164 | } 165 | compareFields(item.Fields, kubeSecret.Data, t) 166 | } 167 | 168 | func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) { 169 | name := "inV@l1d k8s secret%name" 170 | expectedName := "inv-l1d-k8s-secret-name" 171 | namespace := "someNamespace" 172 | annotations := map[string]string{ 173 | "annotationKey": "annotationValue", 174 | } 175 | labels := map[string]string{} 176 | item := onepassword.Item{} 177 | secretType := "" 178 | 179 | item.Fields = []*onepassword.ItemField{ 180 | { 181 | Label: "label w%th invalid ch!rs-", 182 | Value: "value1", 183 | }, 184 | { 185 | Label: strings.Repeat("x", kubeValidate.DNS1123SubdomainMaxLength+1), 186 | Value: "name exceeds max length", 187 | }, 188 | } 189 | 190 | kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil) 191 | 192 | // Assert Secret's meta.name was fixed 193 | if kubeSecret.Name != expectedName { 194 | t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) 195 | } 196 | if kubeSecret.Namespace != namespace { 197 | t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace) 198 | } 199 | 200 | // assert labels were fixed for each data key 201 | for key := range kubeSecret.Data { 202 | if !validLabel(key) { 203 | t.Errorf("Expected valid kubernetes label, got %s", key) 204 | } 205 | } 206 | } 207 | 208 | func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) { 209 | secretName := "tls-test-secret-name" 210 | namespace := "test" 211 | 212 | item := onepassword.Item{} 213 | item.Fields = generateFields(5) 214 | item.Version = 123 215 | item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" 216 | item.ID = "h46bb3jddvay7nxopfhvlwg35q" 217 | 218 | kubeClient := fake.NewClientBuilder().Build() 219 | secretLabels := map[string]string{} 220 | secretType := "kubernetes.io/tls" 221 | 222 | err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) 223 | if err != nil { 224 | t.Errorf("Unexpected error: %v", err) 225 | } 226 | createdSecret := &corev1.Secret{} 227 | err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) 228 | 229 | if err != nil { 230 | t.Errorf("Secret was not created: %v", err) 231 | } 232 | 233 | if createdSecret.Type != corev1.SecretTypeTLS { 234 | t.Errorf("Expected secretType to be of tyype corev1.SecretTypeTLS, got %s", string(createdSecret.Type)) 235 | } 236 | } 237 | 238 | func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { 239 | actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) 240 | if err != nil { 241 | t.Errorf("Was unable to parse Item Path") 242 | } 243 | if actualVaultId != item.Vault.ID { 244 | t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId) 245 | } 246 | if actualItemId != item.ID { 247 | t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId) 248 | } 249 | if annotations[VersionAnnotation] != fmt.Sprint(item.Version) { 250 | t.Errorf("Expected annotation version to be %v but was %v", item.Version, annotations[VersionAnnotation]) 251 | } 252 | 253 | if annotations[RestartDeploymentsAnnotation] != "false" { 254 | t.Errorf("Expected restart deployments annotation to be %v but was %v", restartDeploymentAnnotation, RestartDeploymentsAnnotation) 255 | } 256 | } 257 | 258 | func compareFields(actualFields []*onepassword.ItemField, secretData map[string][]byte, t *testing.T) { 259 | for i := 0; i < len(actualFields); i++ { 260 | value, found := secretData[actualFields[i].Label] 261 | if !found { 262 | t.Errorf("Expected key %v is missing from secret data", actualFields[i].Label) 263 | } 264 | if string(value) != actualFields[i].Value { 265 | t.Errorf("Expected value %v but got %v", actualFields[i].Value, value) 266 | } 267 | } 268 | } 269 | 270 | func generateFields(numToGenerate int) []*onepassword.ItemField { 271 | fields := []*onepassword.ItemField{} 272 | for i := 0; i < numToGenerate; i++ { 273 | field := onepassword.ItemField{ 274 | Label: "key" + fmt.Sprint(i), 275 | Value: "value" + fmt.Sprint(i), 276 | } 277 | fields = append(fields, &field) 278 | } 279 | return fields 280 | } 281 | 282 | func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) { 283 | splitPath := strings.Split(path, "/") 284 | if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { 285 | return splitPath[1], splitPath[3], nil 286 | } 287 | return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) 288 | } 289 | 290 | func validLabel(v string) bool { 291 | if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 { 292 | return false 293 | } 294 | return true 295 | } 296 | -------------------------------------------------------------------------------- /pkg/logs/log_levels.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | // A Level is a logging priority. Lower levels are more important. 4 | // All levels have been multipled by -1 to ensure compatibilty 5 | // between zapcore and logr 6 | const ( 7 | ErrorLevel = -2 8 | WarnLevel = -1 9 | InfoLevel = 0 10 | DebugLevel = 1 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/mocks/mocksecretserver.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/1Password/connect-sdk-go/onepassword" 5 | ) 6 | 7 | type TestClient struct { 8 | GetVaultsFunc func() ([]onepassword.Vault, error) 9 | GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) 10 | GetVaultFunc func(uuid string) (*onepassword.Vault, error) 11 | GetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error) 12 | GetVaultByTitleFunc func(title string) (*onepassword.Vault, error) 13 | GetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error) 14 | GetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error) 15 | GetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error) 16 | GetItemsFunc func(vaultQuery string) ([]onepassword.Item, error) 17 | GetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error) 18 | CreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) 19 | UpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) 20 | DeleteItemFunc func(item *onepassword.Item, vaultQuery string) error 21 | DeleteItemByIDFunc func(itemUUID string, vaultQuery string) error 22 | DeleteItemByTitleFunc func(title string, vaultQuery string) error 23 | GetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error) 24 | GetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) 25 | GetFileContentFunc func(file *onepassword.File) ([]byte, error) 26 | DownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) 27 | LoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error 28 | LoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error 29 | LoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error 30 | LoadStructFunc func(config interface{}) error 31 | } 32 | 33 | var ( 34 | DoGetVaultsFunc func() ([]onepassword.Vault, error) 35 | DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) 36 | DoGetVaultFunc func(uuid string) (*onepassword.Vault, error) 37 | DoGetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error) 38 | DoGetVaultByTitleFunc func(title string) (*onepassword.Vault, error) 39 | DoGetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error) 40 | DoGetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error) 41 | DoGetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error) 42 | DoGetItemsFunc func(vaultQuery string) ([]onepassword.Item, error) 43 | DoGetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error) 44 | DoCreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) 45 | DoUpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) 46 | DoDeleteItemFunc func(item *onepassword.Item, vaultQuery string) error 47 | DoDeleteItemByIDFunc func(itemUUID string, vaultQuery string) error 48 | DoDeleteItemByTitleFunc func(title string, vaultQuery string) error 49 | DoGetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error) 50 | DoGetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) 51 | DoGetFileContentFunc func(file *onepassword.File) ([]byte, error) 52 | DoDownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) 53 | DoLoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error 54 | DoLoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error 55 | DoLoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error 56 | DoLoadStructFunc func(config interface{}) error 57 | ) 58 | 59 | // Do is the mock client's `Do` func 60 | 61 | func (m *TestClient) GetVaults() ([]onepassword.Vault, error) { 62 | return DoGetVaultsFunc() 63 | } 64 | 65 | func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { 66 | return DoGetVaultsByTitleFunc(title) 67 | } 68 | 69 | func (m *TestClient) GetVault(vaultQuery string) (*onepassword.Vault, error) { 70 | return DoGetVaultFunc(vaultQuery) 71 | } 72 | 73 | func (m *TestClient) GetVaultByUUID(uuid string) (*onepassword.Vault, error) { 74 | return DoGetVaultByUUIDFunc(uuid) 75 | } 76 | 77 | func (m *TestClient) GetVaultByTitle(title string) (*onepassword.Vault, error) { 78 | return DoGetVaultByTitleFunc(title) 79 | } 80 | 81 | func (m *TestClient) GetItem(itemQuery string, vaultQuery string) (*onepassword.Item, error) { 82 | return DoGetItemFunc(itemQuery, vaultQuery) 83 | } 84 | 85 | func (m *TestClient) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) { 86 | return DoGetItemByUUIDFunc(uuid, vaultQuery) 87 | } 88 | 89 | func (m *TestClient) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) { 90 | return DoGetItemByTitleFunc(title, vaultQuery) 91 | } 92 | 93 | func (m *TestClient) GetItems(vaultQuery string) ([]onepassword.Item, error) { 94 | return DoGetItemsFunc(vaultQuery) 95 | } 96 | 97 | func (m *TestClient) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) { 98 | return DoGetItemsByTitleFunc(title, vaultQuery) 99 | } 100 | 101 | func (m *TestClient) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { 102 | return DoCreateItemFunc(item, vaultQuery) 103 | } 104 | 105 | func (m *TestClient) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) { 106 | return DoUpdateItemFunc(item, vaultQuery) 107 | } 108 | 109 | func (m *TestClient) DeleteItem(item *onepassword.Item, vaultQuery string) error { 110 | return DoDeleteItemFunc(item, vaultQuery) 111 | } 112 | 113 | func (m *TestClient) DeleteItemByID(itemUUID string, vaultQuery string) error { 114 | return DoDeleteItemByIDFunc(itemUUID, vaultQuery) 115 | } 116 | 117 | func (m *TestClient) DeleteItemByTitle(title string, vaultQuery string) error { 118 | return DoDeleteItemByTitleFunc(title, vaultQuery) 119 | } 120 | 121 | func (m *TestClient) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) { 122 | return DoGetFilesFunc(itemQuery, vaultQuery) 123 | } 124 | 125 | func (m *TestClient) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) { 126 | return DoGetFileFunc(uuid, itemQuery, vaultQuery) 127 | } 128 | 129 | func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) { 130 | return DoGetFileContentFunc(file) 131 | } 132 | 133 | func (m *TestClient) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) { 134 | return DoDownloadFileFunc(file, targetDirectory, overwrite) 135 | } 136 | 137 | func (m *TestClient) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error { 138 | return DoLoadStructFromItemByUUIDFunc(config, itemUUID, vaultQuery) 139 | } 140 | 141 | func (m *TestClient) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error { 142 | return DoLoadStructFromItemByTitleFunc(config, itemTitle, vaultQuery) 143 | } 144 | 145 | func (m *TestClient) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error { 146 | return DoLoadStructFromItemFunc(config, itemQuery, vaultQuery) 147 | } 148 | 149 | func (m *TestClient) LoadStruct(config interface{}) error { 150 | return DoLoadStructFunc(config) 151 | } 152 | -------------------------------------------------------------------------------- /pkg/onepassword/annotations.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "regexp" 5 | 6 | appsv1 "k8s.io/api/apps/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | const ( 11 | OnepasswordPrefix = "operator.1password.io" 12 | ItemPathAnnotation = OnepasswordPrefix + "/item-path" 13 | NameAnnotation = OnepasswordPrefix + "/item-name" 14 | VersionAnnotation = OnepasswordPrefix + "/item-version" 15 | RestartAnnotation = OnepasswordPrefix + "/last-restarted" 16 | RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" 17 | ) 18 | 19 | func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Regexp) (map[string]string, bool) { 20 | annotationsFound := false 21 | annotations := FilterAnnotations(deployment.Annotations, regex) 22 | if len(annotations) > 0 { 23 | annotationsFound = true 24 | } else { 25 | annotations = FilterAnnotations(deployment.Spec.Template.Annotations, regex) 26 | if len(annotations) > 0 { 27 | annotationsFound = true 28 | } else { 29 | annotationsFound = false 30 | } 31 | } 32 | 33 | return annotations, annotationsFound 34 | } 35 | 36 | func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[string]string { 37 | filteredAnnotations := make(map[string]string) 38 | for key, value := range annotations { 39 | if regex.MatchString(key) && key != RestartAnnotation && key != RestartDeploymentsAnnotation { 40 | filteredAnnotations[key] = value 41 | } 42 | } 43 | return filteredAnnotations 44 | } 45 | 46 | func AreAnnotationsUsingSecrets(annotations map[string]string, secrets map[string]*corev1.Secret) bool { 47 | _, ok := secrets[annotations[NameAnnotation]] 48 | if ok { 49 | return true 50 | } 51 | return false 52 | } 53 | 54 | func AppendAnnotationUpdatedSecret(annotations map[string]string, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { 55 | secret, ok := secrets[annotations[NameAnnotation]] 56 | if ok { 57 | updatedDeploymentSecrets[secret.Name] = secret 58 | } 59 | return updatedDeploymentSecrets 60 | } 61 | -------------------------------------------------------------------------------- /pkg/onepassword/annotations_test.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | ) 9 | 10 | const AnnotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" 11 | 12 | func TestFilterAnnotations(t *testing.T) { 13 | invalidAnnotation1 := "onepasswordconnect/vaultId" 14 | invalidAnnotation2 := "onepasswordconnectkubernetesSecrets" 15 | 16 | annotations := getValidAnnotations() 17 | annotations[invalidAnnotation1] = "This should be filtered" 18 | annotations[invalidAnnotation2] = "This should be filtered too" 19 | 20 | r, _ := regexp.Compile(AnnotationRegExpString) 21 | filteredAnnotations := FilterAnnotations(annotations, r) 22 | if len(filteredAnnotations) != 2 { 23 | t.Errorf("Unexpected number of filtered annotations returned. Expected 2, got %v", len(filteredAnnotations)) 24 | } 25 | _, found := filteredAnnotations[ItemPathAnnotation] 26 | if !found { 27 | t.Errorf("One Password Annotation was filtered when it should not have been") 28 | } 29 | _, found = filteredAnnotations[NameAnnotation] 30 | if !found { 31 | t.Errorf("One Password Annotation was filtered when it should not have been") 32 | } 33 | } 34 | 35 | func TestGetTopLevelAnnotationsForDeployment(t *testing.T) { 36 | annotations := getValidAnnotations() 37 | expectedNumAnnotations := len(annotations) 38 | r, _ := regexp.Compile(AnnotationRegExpString) 39 | 40 | deployment := &appsv1.Deployment{} 41 | deployment.Annotations = annotations 42 | filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r) 43 | 44 | if !annotationsFound { 45 | t.Errorf("No annotations marked as found") 46 | } 47 | 48 | numAnnotations := len(filteredAnnotations) 49 | if expectedNumAnnotations != numAnnotations { 50 | t.Errorf("Expected %v annotations got %v", expectedNumAnnotations, numAnnotations) 51 | } 52 | } 53 | 54 | func TestGetTemplateAnnotationsForDeployment(t *testing.T) { 55 | annotations := getValidAnnotations() 56 | expectedNumAnnotations := len(annotations) 57 | r, _ := regexp.Compile(AnnotationRegExpString) 58 | 59 | deployment := &appsv1.Deployment{} 60 | deployment.Spec.Template.Annotations = annotations 61 | filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r) 62 | 63 | if !annotationsFound { 64 | t.Errorf("No annotations marked as found") 65 | } 66 | 67 | numAnnotations := len(filteredAnnotations) 68 | if expectedNumAnnotations != numAnnotations { 69 | t.Errorf("Expected %v annotations got %v", expectedNumAnnotations, numAnnotations) 70 | } 71 | } 72 | 73 | func TestGetNoAnnotationsForDeployment(t *testing.T) { 74 | deployment := &appsv1.Deployment{} 75 | r, _ := regexp.Compile(AnnotationRegExpString) 76 | filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r) 77 | 78 | if annotationsFound { 79 | t.Errorf("No annotations should be found") 80 | } 81 | 82 | numAnnotations := len(filteredAnnotations) 83 | if 0 != numAnnotations { 84 | t.Errorf("Expected %v annotations got %v", 0, numAnnotations) 85 | } 86 | } 87 | 88 | func getValidAnnotations() map[string]string { 89 | return map[string]string{ 90 | ItemPathAnnotation: "vaults/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f/items/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f", 91 | NameAnnotation: "secretName", 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/onepassword/connect_setup.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/api/errors" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/types" 12 | "k8s.io/apimachinery/pkg/util/yaml" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | logf "sigs.k8s.io/controller-runtime/pkg/log" 15 | ) 16 | 17 | var logConnectSetup = logf.Log.WithName("ConnectSetup") 18 | var deploymentPath = "../config/connect/deployment.yaml" 19 | var servicePath = "../config/connect/service.yaml" 20 | 21 | func SetupConnect(kubeClient client.Client, deploymentNamespace string) error { 22 | err := setupService(kubeClient, servicePath, deploymentNamespace) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { 36 | existingDeployment := &appsv1.Deployment{} 37 | 38 | // check if deployment has already been created 39 | err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment) 40 | if err != nil { 41 | if errors.IsNotFound(err) { 42 | logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") 43 | return createDeployment(kubeClient, deploymentPath, deploymentNamespace) 44 | } 45 | } 46 | return err 47 | } 48 | 49 | func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { 50 | deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | err = kubeClient.Create(context.Background(), deployment) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*appsv1.Deployment, error) { 64 | f, err := os.Open(deploymentPath) 65 | if err != nil { 66 | return nil, err 67 | } 68 | deployment := &appsv1.Deployment{ 69 | ObjectMeta: v1.ObjectMeta{ 70 | Namespace: deploymentNamespace, 71 | }, 72 | } 73 | 74 | err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return deployment, nil 79 | } 80 | 81 | func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { 82 | existingService := &corev1.Service{} 83 | 84 | //check if service has already been created 85 | err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService) 86 | if err != nil { 87 | if errors.IsNotFound(err) { 88 | logConnectSetup.Info("No existing Connect service found. Creating Service") 89 | return createService(kubeClient, servicePath, deploymentNamespace) 90 | } 91 | } 92 | return err 93 | } 94 | 95 | func createService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { 96 | f, err := os.Open(servicePath) 97 | if err != nil { 98 | return err 99 | } 100 | service := &corev1.Service{ 101 | ObjectMeta: v1.ObjectMeta{ 102 | Namespace: deploymentNamespace, 103 | }, 104 | } 105 | 106 | err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | err = kubeClient.Create(context.Background(), service) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /pkg/onepassword/connect_setup_test.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/types" 11 | "k8s.io/kubectl/pkg/scheme" 12 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 13 | ) 14 | 15 | var defaultNamespacedName = types.NamespacedName{Name: "onepassword-connect", Namespace: "default"} 16 | 17 | func TestServiceSetup(t *testing.T) { 18 | 19 | // Register operator types with the runtime scheme. 20 | s := scheme.Scheme 21 | 22 | // Objects to track in the fake client. 23 | objs := []runtime.Object{} 24 | 25 | // Create a fake client to mock API calls. 26 | client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() 27 | 28 | err := setupService(client, "../../config/connect/service.yaml", defaultNamespacedName.Namespace) 29 | 30 | if err != nil { 31 | t.Errorf("Error Setting Up Connect: %v", err) 32 | } 33 | 34 | // check that service was created 35 | service := &corev1.Service{} 36 | err = client.Get(context.TODO(), defaultNamespacedName, service) 37 | if err != nil { 38 | t.Errorf("Error Setting Up Connect service: %v", err) 39 | } 40 | } 41 | 42 | func TestDeploymentSetup(t *testing.T) { 43 | 44 | // Register operator types with the runtime scheme. 45 | s := scheme.Scheme 46 | 47 | // Objects to track in the fake client. 48 | objs := []runtime.Object{} 49 | 50 | // Create a fake client to mock API calls. 51 | client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() 52 | 53 | err := setupDeployment(client, "../../config/connect/deployment.yaml", defaultNamespacedName.Namespace) 54 | 55 | if err != nil { 56 | t.Errorf("Error Setting Up Connect: %v", err) 57 | } 58 | 59 | // check that deployment was created 60 | deployment := &appsv1.Deployment{} 61 | err = client.Get(context.TODO(), defaultNamespacedName, deployment) 62 | if err != nil { 63 | t.Errorf("Error Setting Up Connect deployment: %v", err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/onepassword/containers.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | ) 6 | 7 | func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool { 8 | for i := 0; i < len(containers); i++ { 9 | envVariables := containers[i].Env 10 | for j := 0; j < len(envVariables); j++ { 11 | if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil { 12 | _, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name] 13 | if ok { 14 | return true 15 | } 16 | } 17 | } 18 | envFromVariables := containers[i].EnvFrom 19 | for j := 0; j < len(envFromVariables); j++ { 20 | if envFromVariables[j].SecretRef != nil { 21 | _, ok := secrets[envFromVariables[j].SecretRef.Name] 22 | if ok { 23 | return true 24 | } 25 | } 26 | } 27 | } 28 | return false 29 | } 30 | 31 | func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { 32 | for i := 0; i < len(containers); i++ { 33 | envVariables := containers[i].Env 34 | for j := 0; j < len(envVariables); j++ { 35 | if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil { 36 | secret, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name] 37 | if ok { 38 | updatedDeploymentSecrets[secret.Name] = secret 39 | } 40 | } 41 | } 42 | envFromVariables := containers[i].EnvFrom 43 | for j := 0; j < len(envFromVariables); j++ { 44 | if envFromVariables[j].SecretRef != nil { 45 | secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name] 46 | if ok { 47 | updatedDeploymentSecrets[secret.Name] = secret 48 | } 49 | } 50 | } 51 | } 52 | return updatedDeploymentSecrets 53 | } 54 | -------------------------------------------------------------------------------- /pkg/onepassword/containers_test.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "testing" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | func TestAreContainersUsingSecretsFromEnv(t *testing.T) { 11 | secretNamesToSearch := map[string]*corev1.Secret{ 12 | "onepassword-database-secret": {}, 13 | "onepassword-api-key": {}, 14 | } 15 | 16 | containerSecretNames := []string{ 17 | "onepassword-database-secret", 18 | "onepassword-api-key", 19 | "some_other_key", 20 | } 21 | 22 | containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) 23 | 24 | if !AreContainersUsingSecrets(containers, secretNamesToSearch) { 25 | t.Errorf("Expected that containers were using secrets but they were not detected.") 26 | } 27 | } 28 | 29 | func TestAreContainersUsingSecretsFromEnvFrom(t *testing.T) { 30 | secretNamesToSearch := map[string]*corev1.Secret{ 31 | "onepassword-database-secret": {}, 32 | "onepassword-api-key": {}, 33 | } 34 | 35 | containerSecretNames := []string{ 36 | "onepassword-database-secret", 37 | "onepassword-api-key", 38 | "some_other_key", 39 | } 40 | 41 | containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) 42 | 43 | if !AreContainersUsingSecrets(containers, secretNamesToSearch) { 44 | t.Errorf("Expected that containers were using secrets but they were not detected.") 45 | } 46 | } 47 | 48 | func TestAreContainersNotUsingSecrets(t *testing.T) { 49 | secretNamesToSearch := map[string]*corev1.Secret{ 50 | "onepassword-database-secret": {}, 51 | "onepassword-api-key": {}, 52 | } 53 | 54 | containerSecretNames := []string{ 55 | "some_other_key", 56 | } 57 | 58 | containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) 59 | 60 | if AreContainersUsingSecrets(containers, secretNamesToSearch) { 61 | t.Errorf("Expected that containers were not using secrets but they were detected.") 62 | } 63 | } 64 | 65 | func TestAppendUpdatedContainerSecretsParsesEnvFromEnv(t *testing.T) { 66 | secretNamesToSearch := map[string]*corev1.Secret{ 67 | "onepassword-database-secret": {}, 68 | "onepassword-api-key": {ObjectMeta: metav1.ObjectMeta{Name: "onepassword-api-key"}}, 69 | } 70 | 71 | containerSecretNames := []string{ 72 | "onepassword-api-key", 73 | } 74 | 75 | containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) 76 | 77 | updatedDeploymentSecrets := map[string]*corev1.Secret{} 78 | updatedDeploymentSecrets = AppendUpdatedContainerSecrets(containers, secretNamesToSearch, updatedDeploymentSecrets) 79 | 80 | secretKeyName := "onepassword-api-key" 81 | 82 | if updatedDeploymentSecrets[secretKeyName] != secretNamesToSearch[secretKeyName] { 83 | t.Errorf("Expected that updated Secret from envfrom is found.") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/onepassword/deployments.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | func IsDeploymentUsingSecrets(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) bool { 9 | volumes := deployment.Spec.Template.Spec.Volumes 10 | containers := deployment.Spec.Template.Spec.Containers 11 | containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) 12 | return AreAnnotationsUsingSecrets(deployment.Annotations, secrets) || AreContainersUsingSecrets(containers, secrets) || AreVolumesUsingSecrets(volumes, secrets) 13 | } 14 | 15 | func GetUpdatedSecretsForDeployment(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) map[string]*corev1.Secret { 16 | volumes := deployment.Spec.Template.Spec.Volumes 17 | containers := deployment.Spec.Template.Spec.Containers 18 | containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) 19 | 20 | updatedSecretsForDeployment := map[string]*corev1.Secret{} 21 | AppendAnnotationUpdatedSecret(deployment.Annotations, secrets, updatedSecretsForDeployment) 22 | AppendUpdatedContainerSecrets(containers, secrets, updatedSecretsForDeployment) 23 | AppendUpdatedVolumeSecrets(volumes, secrets, updatedSecretsForDeployment) 24 | 25 | return updatedSecretsForDeployment 26 | } 27 | -------------------------------------------------------------------------------- /pkg/onepassword/deployments_test.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "testing" 5 | 6 | appsv1 "k8s.io/api/apps/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) { 11 | secretNamesToSearch := map[string]*corev1.Secret{ 12 | "onepassword-database-secret": {}, 13 | "onepassword-api-key": {}, 14 | "onepassword-app-token": {}, 15 | "onepassword-user-credentials": {}, 16 | } 17 | 18 | volumeSecretNames := []string{ 19 | "onepassword-database-secret", 20 | "onepassword-api-key", 21 | } 22 | 23 | volumes := generateVolumes(volumeSecretNames) 24 | 25 | volumeProjectedSecretNames := []string{ 26 | "onepassword-app-token", 27 | "onepassword-user-credentials", 28 | } 29 | 30 | volumeProjected := generateVolumesProjected(volumeProjectedSecretNames) 31 | 32 | volumes = append(volumes, volumeProjected) 33 | 34 | deployment := &appsv1.Deployment{} 35 | deployment.Spec.Template.Spec.Volumes = volumes 36 | if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { 37 | t.Errorf("Expected that deployment was using secrets but they were not detected.") 38 | } 39 | } 40 | 41 | func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { 42 | secretNamesToSearch := map[string]*corev1.Secret{ 43 | "onepassword-database-secret": {}, 44 | "onepassword-api-key": {}, 45 | } 46 | 47 | containerSecretNames := []string{ 48 | "onepassword-database-secret", 49 | "onepassword-api-key", 50 | "some_other_key", 51 | } 52 | 53 | deployment := &appsv1.Deployment{} 54 | deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames) 55 | if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { 56 | t.Errorf("Expected that deployment was using secrets but they were not detected.") 57 | } 58 | } 59 | 60 | func TestIsDeploymentNotUSingSecrets(t *testing.T) { 61 | secretNamesToSearch := map[string]*corev1.Secret{ 62 | "onepassword-database-secret": {}, 63 | "onepassword-api-key": {}, 64 | } 65 | 66 | deployment := &appsv1.Deployment{} 67 | if IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { 68 | t.Errorf("Expected that deployment was using not secrets but they were detected.") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/onepassword/items.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/1Password/connect-sdk-go/connect" 8 | "github.com/1Password/connect-sdk-go/onepassword" 9 | 10 | logf "sigs.k8s.io/controller-runtime/pkg/log" 11 | ) 12 | 13 | var logger = logf.Log.WithName("retrieve_item") 14 | 15 | func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) { 16 | vaultValue, itemValue, err := ParseVaultAndItemFromPath(path) 17 | if err != nil { 18 | return nil, err 19 | } 20 | vaultId, err := getVaultId(opConnectClient, vaultValue) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | itemId, err := getItemId(opConnectClient, itemValue, vaultId) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | item, err := opConnectClient.GetItem(itemId, vaultId) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | for _, file := range item.Files { 36 | _, err := opConnectClient.GetFileContent(file) 37 | if err != nil { 38 | return nil, err 39 | } 40 | } 41 | 42 | return item, nil 43 | } 44 | 45 | func ParseVaultAndItemFromPath(path string) (string, string, error) { 46 | splitPath := strings.Split(path, "/") 47 | if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { 48 | return splitPath[1], splitPath[3], nil 49 | } 50 | return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) 51 | } 52 | 53 | func getVaultId(client connect.Client, vaultIdentifier string) (string, error) { 54 | if !IsValidClientUUID(vaultIdentifier) { 55 | vaults, err := client.GetVaultsByTitle(vaultIdentifier) 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | if len(vaults) == 0 { 61 | return "", fmt.Errorf("No vaults found with identifier %q", vaultIdentifier) 62 | } 63 | 64 | oldestVault := vaults[0] 65 | if len(vaults) > 1 { 66 | for _, returnedVault := range vaults { 67 | if returnedVault.CreatedAt.Before(oldestVault.CreatedAt) { 68 | oldestVault = returnedVault 69 | } 70 | } 71 | logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultIdentifier, oldestVault.ID)) 72 | } 73 | vaultIdentifier = oldestVault.ID 74 | } 75 | return vaultIdentifier, nil 76 | } 77 | 78 | func getItemId(client connect.Client, itemIdentifier string, vaultId string) (string, error) { 79 | if !IsValidClientUUID(itemIdentifier) { 80 | items, err := client.GetItemsByTitle(itemIdentifier, vaultId) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | if len(items) == 0 { 86 | return "", fmt.Errorf("No items found with identifier %q", itemIdentifier) 87 | } 88 | 89 | oldestItem := items[0] 90 | if len(items) > 1 { 91 | for _, returnedItem := range items { 92 | if returnedItem.CreatedAt.Before(oldestItem.CreatedAt) { 93 | oldestItem = returnedItem 94 | } 95 | } 96 | logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemIdentifier, oldestItem.ID)) 97 | } 98 | itemIdentifier = oldestItem.ID 99 | } 100 | return itemIdentifier, nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/onepassword/object_generators_for_test.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import corev1 "k8s.io/api/core/v1" 4 | 5 | func generateVolumes(names []string) []corev1.Volume { 6 | volumes := []corev1.Volume{} 7 | for i := 0; i < len(names); i++ { 8 | volume := corev1.Volume{ 9 | Name: names[i], 10 | VolumeSource: corev1.VolumeSource{ 11 | Secret: &corev1.SecretVolumeSource{ 12 | SecretName: names[i], 13 | }, 14 | }, 15 | } 16 | volumes = append(volumes, volume) 17 | } 18 | return volumes 19 | } 20 | func generateVolumesProjected(names []string) corev1.Volume { 21 | volumesProjection := []corev1.VolumeProjection{} 22 | for i := 0; i < len(names); i++ { 23 | volumeProjection := corev1.VolumeProjection{ 24 | Secret: &corev1.SecretProjection{ 25 | LocalObjectReference: corev1.LocalObjectReference{ 26 | Name: names[i], 27 | }, 28 | }, 29 | } 30 | volumesProjection = append(volumesProjection, volumeProjection) 31 | } 32 | volume := corev1.Volume{ 33 | Name: "someName", 34 | VolumeSource: corev1.VolumeSource{ 35 | Projected: &corev1.ProjectedVolumeSource{ 36 | Sources: volumesProjection, 37 | }, 38 | }, 39 | } 40 | 41 | return volume 42 | } 43 | func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container { 44 | containers := []corev1.Container{} 45 | for i := 0; i < len(names); i++ { 46 | container := corev1.Container{ 47 | Env: []corev1.EnvVar{ 48 | { 49 | Name: "someName", 50 | ValueFrom: &corev1.EnvVarSource{ 51 | SecretKeyRef: &corev1.SecretKeySelector{ 52 | LocalObjectReference: corev1.LocalObjectReference{ 53 | Name: names[i], 54 | }, 55 | Key: "password", 56 | }, 57 | }, 58 | }, 59 | }, 60 | } 61 | containers = append(containers, container) 62 | } 63 | return containers 64 | } 65 | 66 | func generateContainersWithSecretRefsFromEnvFrom(names []string) []corev1.Container { 67 | containers := []corev1.Container{} 68 | for i := 0; i < len(names); i++ { 69 | container := corev1.Container{ 70 | EnvFrom: []corev1.EnvFromSource{ 71 | {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: names[i]}}}, 72 | }, 73 | } 74 | containers = append(containers, container) 75 | } 76 | return containers 77 | } 78 | -------------------------------------------------------------------------------- /pkg/onepassword/secret_update_handler.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" 9 | kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" 10 | "github.com/1Password/onepassword-operator/pkg/logs" 11 | "github.com/1Password/onepassword-operator/pkg/utils" 12 | 13 | "github.com/1Password/connect-sdk-go/connect" 14 | "github.com/1Password/connect-sdk-go/onepassword" 15 | appsv1 "k8s.io/api/apps/v1" 16 | corev1 "k8s.io/api/core/v1" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | logf "sigs.k8s.io/controller-runtime/pkg/log" 19 | ) 20 | 21 | const envHostVariable = "OP_HOST" 22 | const lockTag = "operator.1password.io:ignore-secret" 23 | 24 | var log = logf.Log.WithName("update_op_kubernetes_secrets_task") 25 | 26 | func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler { 27 | return &SecretUpdateHandler{ 28 | client: kubernetesClient, 29 | opConnectClient: opConnectClient, 30 | shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal, 31 | } 32 | } 33 | 34 | type SecretUpdateHandler struct { 35 | client client.Client 36 | opConnectClient connect.Client 37 | shouldAutoRestartDeploymentsGlobal bool 38 | } 39 | 40 | func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error { 41 | updatedKubernetesSecrets, err := h.updateKubernetesSecrets() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets) 47 | } 48 | 49 | func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret) error { 50 | // No secrets to update. Exit 51 | if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil { 52 | return nil 53 | } 54 | 55 | deployments := &appsv1.DeploymentList{} 56 | err := h.client.List(context.Background(), deployments) 57 | if err != nil { 58 | log.Error(err, "Failed to list kubernetes deployments") 59 | return err 60 | } 61 | 62 | if len(deployments.Items) == 0 { 63 | return nil 64 | } 65 | 66 | setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | for i := 0; i < len(deployments.Items); i++ { 72 | deployment := &deployments.Items[i] 73 | updatedSecrets := updatedSecretsByNamespace[deployment.Namespace] 74 | 75 | updatedDeploymentSecrets := GetUpdatedSecretsForDeployment(deployment, updatedSecrets) 76 | if len(updatedDeploymentSecrets) == 0 { 77 | continue 78 | } 79 | for _, secret := range updatedDeploymentSecrets { 80 | if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) { 81 | h.restartDeployment(deployment) 82 | continue 83 | } 84 | } 85 | 86 | log.V(logs.DebugLevel).Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace)) 87 | 88 | } 89 | return nil 90 | } 91 | 92 | func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) { 93 | log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace)) 94 | if deployment.Spec.Template.Annotations == nil { 95 | deployment.Spec.Template.Annotations = map[string]string{} 96 | } 97 | deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String() 98 | err := h.client.Update(context.Background(), deployment) 99 | if err != nil { 100 | log.Error(err, "Problem restarting deployment") 101 | } 102 | } 103 | 104 | func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*corev1.Secret, error) { 105 | secrets := &corev1.SecretList{} 106 | err := h.client.List(context.Background(), secrets) 107 | if err != nil { 108 | log.Error(err, "Failed to list kubernetes secrets") 109 | return nil, err 110 | } 111 | 112 | updatedSecrets := map[string]map[string]*corev1.Secret{} 113 | for i := 0; i < len(secrets.Items); i++ { 114 | secret := secrets.Items[i] 115 | 116 | itemPath := secret.Annotations[ItemPathAnnotation] 117 | currentVersion := secret.Annotations[VersionAnnotation] 118 | if len(itemPath) == 0 || len(currentVersion) == 0 { 119 | continue 120 | } 121 | 122 | OnePasswordItemPath := h.getPathFromOnePasswordItem(secret) 123 | 124 | item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath) 125 | if err != nil { 126 | log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name) 127 | continue 128 | } 129 | 130 | itemVersion := fmt.Sprint(item.Version) 131 | itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID) 132 | 133 | if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString { 134 | if isItemLockedForForcedRestarts(item) { 135 | log.V(logs.DebugLevel).Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName())) 136 | secret.Annotations[VersionAnnotation] = itemVersion 137 | secret.Annotations[ItemPathAnnotation] = itemPathString 138 | if err := h.client.Update(context.Background(), &secret); err != nil { 139 | log.Error(err, "failed to update secret %s annotations to version %d: %s", secret.Name, itemVersion, err) 140 | continue 141 | } 142 | continue 143 | } 144 | log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) 145 | secret.Annotations[VersionAnnotation] = itemVersion 146 | secret.Annotations[ItemPathAnnotation] = itemPathString 147 | secret.Data = kubeSecrets.BuildKubernetesSecretData(item.Fields, item.Files) 148 | log.V(logs.DebugLevel).Info(fmt.Sprintf("New secret path: %v and version: %v", secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation])) 149 | if err := h.client.Update(context.Background(), &secret); err != nil { 150 | log.Error(err, "failed to update secret %s to version %d: %s", secret.Name, itemVersion, err) 151 | continue 152 | } 153 | if updatedSecrets[secret.Namespace] == nil { 154 | updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret) 155 | } 156 | updatedSecrets[secret.Namespace][secret.Name] = &secret 157 | } 158 | } 159 | return updatedSecrets, nil 160 | } 161 | 162 | func isItemLockedForForcedRestarts(item *onepassword.Item) bool { 163 | tags := item.Tags 164 | for i := 0; i < len(tags); i++ { 165 | if tags[i] == lockTag { 166 | return true 167 | } 168 | } 169 | return false 170 | } 171 | 172 | func isUpdatedSecret(secretName string, updatedSecrets map[string]*corev1.Secret) bool { 173 | _, ok := updatedSecrets[secretName] 174 | if ok { 175 | return true 176 | } 177 | return false 178 | } 179 | 180 | func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string]bool, error) { 181 | namespaces := &corev1.NamespaceList{} 182 | err := h.client.List(context.Background(), namespaces) 183 | if err != nil { 184 | log.Error(err, "Failed to list kubernetes namespaces") 185 | return nil, err 186 | } 187 | 188 | namespacesMap := map[string]bool{} 189 | 190 | for _, namespace := range namespaces.Items { 191 | namespacesMap[namespace.Name] = h.isNamespaceSetToAutoRestart(&namespace) 192 | } 193 | return namespacesMap, nil 194 | } 195 | 196 | func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string { 197 | onePasswordItem := &onepasswordv1.OnePasswordItem{} 198 | 199 | // Search for our original OnePasswordItem if it exists 200 | err := h.client.Get(context.TODO(), client.ObjectKey{ 201 | Namespace: secret.Namespace, 202 | Name: secret.Name}, onePasswordItem) 203 | 204 | if err == nil { 205 | return onePasswordItem.Spec.ItemPath 206 | } 207 | 208 | // If we can't find the OnePassword Item we'll just return the annotation from the secret item. 209 | return secret.Annotations[ItemPathAnnotation] 210 | } 211 | 212 | func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { 213 | restartDeployment := secret.Annotations[RestartDeploymentsAnnotation] 214 | //If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace 215 | if restartDeployment == "" { 216 | return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace) 217 | } 218 | 219 | restartDeploymentBool, err := utils.StringToBool(restartDeployment) 220 | if err != nil { 221 | log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secret.Name) 222 | return false 223 | } 224 | return restartDeploymentBool 225 | } 226 | 227 | func isDeploymentSetForAutoRestart(deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { 228 | restartDeployment := deployment.Annotations[RestartDeploymentsAnnotation] 229 | //If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace 230 | if restartDeployment == "" { 231 | return setForAutoRestartByNamespace[deployment.Namespace] 232 | } 233 | 234 | restartDeploymentBool, err := utils.StringToBool(restartDeployment) 235 | if err != nil { 236 | log.Error(err, "Error parsing %v annotation on Deployment %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, deployment.Name) 237 | return false 238 | } 239 | return restartDeploymentBool 240 | } 241 | 242 | func (h *SecretUpdateHandler) isNamespaceSetToAutoRestart(namespace *corev1.Namespace) bool { 243 | restartDeployment := namespace.Annotations[RestartDeploymentsAnnotation] 244 | //If annotation for auto restarts for deployment is not set. Check environment variable set on the operator 245 | if restartDeployment == "" { 246 | return h.shouldAutoRestartDeploymentsGlobal 247 | } 248 | 249 | restartDeploymentBool, err := utils.StringToBool(restartDeployment) 250 | if err != nil { 251 | log.Error(err, "Error parsing %v annotation on Namespace %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, namespace.Name) 252 | return false 253 | } 254 | return restartDeploymentBool 255 | } 256 | -------------------------------------------------------------------------------- /pkg/onepassword/uuid.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | // UUIDLength defines the required length of UUIDs 4 | const UUIDLength = 26 5 | 6 | // IsValidClientUUID returns true if the given client uuid is valid. 7 | func IsValidClientUUID(uuid string) bool { 8 | if len(uuid) != UUIDLength { 9 | return false 10 | } 11 | 12 | for _, c := range uuid { 13 | valid := (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') 14 | if !valid { 15 | return false 16 | } 17 | } 18 | 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /pkg/onepassword/volumes.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import corev1 "k8s.io/api/core/v1" 4 | 5 | func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret) bool { 6 | for i := 0; i < len(volumes); i++ { 7 | secret := IsVolumeUsingSecret(volumes[i], secrets) 8 | secretProjection := IsVolumeUsingSecretProjection(volumes[i], secrets) 9 | if secret == nil && secretProjection == nil { 10 | return false 11 | } 12 | } 13 | if len(volumes) == 0 { 14 | return false 15 | } 16 | return true 17 | } 18 | 19 | func AppendUpdatedVolumeSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { 20 | for i := 0; i < len(volumes); i++ { 21 | secret := IsVolumeUsingSecret(volumes[i], secrets) 22 | if secret != nil { 23 | updatedDeploymentSecrets[secret.Name] = secret 24 | } else { 25 | secretProjection := IsVolumeUsingSecretProjection(volumes[i], secrets) 26 | if secretProjection != nil { 27 | updatedDeploymentSecrets[secretProjection.Name] = secretProjection 28 | } 29 | } 30 | } 31 | return updatedDeploymentSecrets 32 | } 33 | 34 | func IsVolumeUsingSecret(volume corev1.Volume, secrets map[string]*corev1.Secret) *corev1.Secret { 35 | if secret := volume.Secret; secret != nil { 36 | secretName := secret.SecretName 37 | secretFound, ok := secrets[secretName] 38 | if ok { 39 | return secretFound 40 | } 41 | } 42 | return nil 43 | } 44 | 45 | func IsVolumeUsingSecretProjection(volume corev1.Volume, secrets map[string]*corev1.Secret) *corev1.Secret { 46 | if volume.Projected != nil { 47 | for i := 0; i < len(volume.Projected.Sources); i++ { 48 | if secret := volume.Projected.Sources[i].Secret; secret != nil { 49 | secretName := secret.Name 50 | secretFound, ok := secrets[secretName] 51 | if ok { 52 | return secretFound 53 | } 54 | } 55 | } 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/onepassword/volumes_test.go: -------------------------------------------------------------------------------- 1 | package onepassword 2 | 3 | import ( 4 | "testing" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | func TestAreVolmesUsingSecrets(t *testing.T) { 10 | secretNamesToSearch := map[string]*corev1.Secret{ 11 | "onepassword-database-secret": {}, 12 | "onepassword-api-key": {}, 13 | "onepassword-app-token": {}, 14 | "onepassword-user-credentials": {}, 15 | } 16 | 17 | volumeSecretNames := []string{ 18 | "onepassword-database-secret", 19 | "onepassword-api-key", 20 | } 21 | 22 | volumes := generateVolumes(volumeSecretNames) 23 | 24 | volumeProjectedSecretNames := []string{ 25 | "onepassword-app-token", 26 | "onepassword-user-credentials", 27 | } 28 | 29 | volumeProjected := generateVolumesProjected(volumeProjectedSecretNames) 30 | 31 | volumes = append(volumes, volumeProjected) 32 | 33 | if !AreVolumesUsingSecrets(volumes, secretNamesToSearch) { 34 | t.Errorf("Expected that volumes were using secrets but they were not detected.") 35 | } 36 | } 37 | 38 | func TestAreVolumesNotUsingSecrets(t *testing.T) { 39 | secretNamesToSearch := map[string]*corev1.Secret{ 40 | "onepassword-database-secret": {}, 41 | "onepassword-api-key": {}, 42 | } 43 | 44 | volumeSecretNames := []string{ 45 | "some_other_key", 46 | } 47 | 48 | volumes := generateVolumes(volumeSecretNames) 49 | 50 | if AreVolumesUsingSecrets(volumes, secretNamesToSearch) { 51 | t.Errorf("Expected that volumes were not using secrets but they were detected.") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/utils/k8sutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Operator-SDK Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strings" 21 | 22 | "github.com/1Password/onepassword-operator/pkg/logs" 23 | logf "sigs.k8s.io/controller-runtime/pkg/log" 24 | ) 25 | 26 | var ForceRunModeEnv = "OSDK_FORCE_RUN_MODE" 27 | 28 | type RunModeType string 29 | 30 | const ( 31 | LocalRunMode RunModeType = "local" 32 | ClusterRunMode RunModeType = "cluster" 33 | ) 34 | 35 | var log = logf.Log.WithName("k8sutil") 36 | 37 | // ErrNoNamespace indicates that a namespace could not be found for the current 38 | // environment 39 | var ErrNoNamespace = fmt.Errorf("namespace not found for current environment") 40 | 41 | // ErrRunLocal indicates that the operator is set to run in local mode (this error 42 | // is returned by functions that only work on operators running in cluster mode) 43 | var ErrRunLocal = fmt.Errorf("operator run mode forced to local") 44 | 45 | // GetOperatorNamespace returns the namespace the operator should be running in. 46 | func GetOperatorNamespace() (string, error) { 47 | if isRunModeLocal() { 48 | return "", ErrRunLocal 49 | } 50 | nsBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") 51 | if err != nil { 52 | if os.IsNotExist(err) { 53 | return "", ErrNoNamespace 54 | } 55 | return "", err 56 | } 57 | ns := strings.TrimSpace(string(nsBytes)) 58 | log.V(logs.DebugLevel).Info("Found namespace", "Namespace", ns) 59 | return ns, nil 60 | } 61 | 62 | func isRunModeLocal() bool { 63 | return os.Getenv(ForceRunModeEnv) == string(LocalRunMode) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/utils/string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | func ContainsString(slice []string, s string) bool { 9 | for _, item := range slice { 10 | if item == s { 11 | return true 12 | } 13 | } 14 | return false 15 | } 16 | 17 | func RemoveString(slice []string, s string) (result []string) { 18 | for _, item := range slice { 19 | if item == s { 20 | continue 21 | } 22 | result = append(result, item) 23 | } 24 | return 25 | } 26 | 27 | func StringToBool(str string) (bool, error) { 28 | restartDeploymentBool, err := strconv.ParseBool(strings.ToLower(str)) 29 | if err != nil { 30 | return false, err 31 | } 32 | return restartDeploymentBool, nil 33 | } 34 | -------------------------------------------------------------------------------- /scripts/prepare-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # prepare-release.sh 4 | # (Note: This should be called by `make release/prepare` because it depends 5 | # on several variables set by the Makefile) 6 | # 7 | # Performs release preparation tasks: 8 | # - Creates a release branch 9 | # - Renames "LATEST" section to the new version number 10 | # - Adds new "LATEST" entry to the changelog 11 | # 12 | ############################################## 13 | set -Eeuo pipefail 14 | 15 | if [[ -z "${NEW_VERSION:-}" ]]; then 16 | echo "[ERROR] NEW_VERSION environment variable not defined." >&2 17 | exit 1 18 | fi 19 | 20 | # Script called from within a git repo? 21 | if [[ $(git rev-parse --is-inside-work-tree &>/dev/null) -ne 0 ]]; then 22 | echo "[ERROR] Current directory (${SRCDIR}) is not a git repository" >&2 23 | exit 1 24 | fi 25 | 26 | REPO_ROOT=$(git rev-parse --show-toplevel) 27 | CHANGELOG_FILENAME=${CHANGELOG:-"CHANGELOG.md"} 28 | 29 | # normalize version by removing `v` prefix 30 | VERSION_NUM=${NEW_VERSION/#v/} 31 | RELEASE_BRANCH=$(printf "release/v%s" "${VERSION_NUM}") 32 | 33 | function updateChangelog() { 34 | local tmpfile 35 | 36 | trap '[ -e "${tmpfile}" ] && rm "${tmpfile}"' RETURN 37 | 38 | local changelogFile 39 | changelogFile=$(printf "%s/%s" "${REPO_ROOT}" "${CHANGELOG_FILENAME}") 40 | 41 | # create Changelog file if not exists 42 | if ! [[ -f "${REPO_ROOT}/${CHANGELOG_FILENAME}" ]]; then 43 | touch "${REPO_ROOT}/${CHANGELOG_FILENAME}" && \ 44 | git add "${REPO_ROOT}/${CHANGELOG_FILENAME}" 45 | fi 46 | 47 | tmpfile=$(mktemp) 48 | 49 | # Replace "Latest" in the top-most changelog block with new version 50 | # Then push a new "latest" block to top of the changelog 51 | awk 'NR==1, /---/{ sub(/START\/LATEST/, "START/v'${VERSION_NUM}'"); sub(/# Latest/, "# v'${VERSION_NUM}'") } {print}' \ 52 | "${changelogFile}" > "${tmpfile}" 53 | 54 | # Inserts "Latest" changelog HEREDOC at the top of the file 55 | cat - "${tmpfile}" << EOF > "${REPO_ROOT}/${CHANGELOG_FILENAME}" 56 | [//]: # (START/LATEST) 57 | # Latest 58 | 59 | ## Features 60 | * A user-friendly description of a new feature. {issue-number} 61 | 62 | ## Fixes 63 | * A user-friendly description of a fix. {issue-number} 64 | 65 | ## Security 66 | * A user-friendly description of a security fix. {issue-number} 67 | 68 | --- 69 | 70 | EOF 71 | } 72 | 73 | function _main() { 74 | 75 | # Stash version changes 76 | git stash push &>/dev/null 77 | 78 | if ! git checkout -b "${RELEASE_BRANCH}" origin/"${MAIN_BRANCH:-main}"; then 79 | echo "[ERROR] Could not check out release branch." >&2 80 | git stash pop &>/dev/null 81 | exit 1 82 | fi 83 | 84 | # Add the version changes to release branch 85 | git stash pop &>/dev/null 86 | 87 | updateChangelog 88 | 89 | cat << EOF 90 | 91 | [SUCCESS] Changelog updated & release branch created: 92 | New Version: ${NEW_VERSION} 93 | Release Branch: ${RELEASE_BRANCH} 94 | 95 | Next steps: 96 | 1. Edit the changelog notes in ${CHANGELOG_FILENAME} 97 | 2. Commit changes to the release branch 98 | 3. Push changes to remote => git push origin ${RELEASE_BRANCH} 99 | 100 | EOF 101 | exit 0 102 | } 103 | 104 | _main 105 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | OperatorVersion = "1.8.1" 5 | OperatorSDKVersion = "1.34.1" 6 | ) 7 | --------------------------------------------------------------------------------