mirror of
				https://github.com/1Password/onepassword-operator.git
				synced 2025-10-31 19:59:40 +00:00 
			
		
		
		
	Compare commits
	
		
			155 Commits
		
	
	
		
			v0.0.1
			...
			feature/mi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 209bc7cd17 | ||
|   | 493b311564 | ||
|   | e39cff881d | ||
|   | 0796b9c5e2 | ||
|   | 37a0f4b51e | ||
|   | 004e0101ff | ||
|   | 6326a856ae | ||
|   | 1ddf92c5a0 | ||
|   | f5c6fa5860 | ||
|   | afa076d321 | ||
|   | d4b04c233c | ||
|   | ea68cfc2b4 | ||
|   | 1a085562e4 | ||
|   | 21111fec90 | ||
|   | 69cc7cedb0 | ||
|   | b30c6130f7 | ||
|   | 58b4ff8ecf | ||
|   | d93fecdc76 | ||
|   | 486465247d | ||
|   | 79868ae374 | ||
|   | 6286f7e306 | ||
|   | 0b5efc8690 | ||
|   | c00baeedcb | ||
|   | a37bddbfd9 | ||
|   | bd9922f635 | ||
|   | 8fa4413880 | ||
|   | 62e55a3f19 | ||
|   | d6f7b80c40 | ||
|   | a903f9b1af | ||
|   | 5cddc9d8a9 | ||
|   | 7e1b94fae7 | ||
|   | 6953a89c89 | ||
|   | 0d9e07f543 | ||
|   | 098d504d2a | ||
|   | b68d9a5d79 | ||
|   | befcaae457 | ||
|   | b24aa48bd6 | ||
|   | b1e251dee6 | ||
|   | a34c6e8b38 | ||
|   | b16960057a | ||
|   | 285496dc7e | ||
|   | f38cf7e1c2 | ||
|   | bb7a0c8ca9 | ||
|   | 302653832e | ||
|   | a1bcfdfdcb | ||
|   | c0f1632638 | ||
|   | c46065fa7a | ||
|   | 5d229c42d5 | ||
|   | c7235b4f09 | ||
|   | 5183fc129a | ||
|   | 7d619165b2 | ||
|   | 0363ae1e4e | ||
|   | d9e003bdb7 | ||
|   | b25f943b3a | ||
|   | 5fab662424 | ||
|   | d807e92c36 | ||
|   | 244771717c | ||
|   | a760e524ea | ||
|   | 7aeb36e383 | ||
|   | 5c2f840623 | ||
|   | 670040477e | ||
|   | a45a310611 | ||
|   | d80e8dd799 | ||
|   | 88728909ff | ||
|   | e365ebfdfa | ||
|   | 2c4b4df01a | ||
|   | 49d984c6f2 | ||
|   | 72cad7284c | ||
|   | 19f774bb2d | ||
|   | 0193a98681 | ||
|   | f241d7423d | ||
|   | 6043e0da0b | ||
|   | 753cc5e9a3 | ||
|   | 8cfe98073e | ||
|   | c0037526b0 | ||
|   | 96b42e7c52 | ||
|   | 579b5848da | ||
|   | dff934cbc3 | ||
|   | 2096f4440f | ||
|   | b3fc707337 | ||
|   | 32643651d9 | ||
|   | ba8d3fa698 | ||
|   | c57aa22a9c | ||
|   | 48944b0d56 | ||
|   | 313cd1169b | ||
|   | b50d864b50 | ||
|   | 1643385d9b | ||
|   | 9441214733 | ||
|   | 7e4e988813 | ||
|   | fb1262f1bd | ||
|   | 68f084080e | ||
|   | a428fe7462 | ||
|   | ea2d1f8a09 | ||
|   | bd96d50a9b | ||
|   | 859c9e3462 | ||
|   | 9dabac4a55 | ||
|   | d927a08790 | ||
|   | 933f7c4e2c | ||
|   | 81eb9a521f | ||
|   | eb32bd7f94 | ||
|   | a5781af949 | ||
|   | 0aa5781acd | ||
|   | 700be4426f | ||
|   | 76ef9aa372 | ||
|   | d7e6704314 | ||
|   | 2443979602 | ||
|   | 5b65196d31 | ||
|   | e7df8a485d | ||
|   | ded76138da | ||
|   | a5db6aeb81 | ||
|   | d45f682c37 | ||
|   | d0c1235e58 | ||
|   | 9e8f621020 | ||
|   | 8dd7a28456 | ||
|   | 43b06dd7aa | ||
|   | e8e01d6578 | ||
|   | b53e017b77 | ||
|   | b2565cebf8 | ||
|   | 9459d2e292 | ||
|   | 0409b17ef4 | ||
|   | 2e47b76d4c | ||
|   | 1cca09df90 | ||
|   | b9cb92eb1b | ||
|   | b574e394ad | ||
|   | 2b92214cf5 | ||
|   | 5422e37d77 | ||
|   | ac05846062 | ||
|   | dcc37519ef | ||
|   | 73b089df79 | ||
|   | 97650573fd | ||
|   | 42e348de91 | ||
|   | 9946ce7ba6 | ||
|   | 71ccfc6235 | ||
|   | 6cb8b87560 | ||
|   | 62ca0c25fd | ||
|   | 990ac86297 | ||
|   | 077142d9f2 | ||
|   | d8a969265c | ||
|   | d98f9172a0 | ||
|   | 8635be0cab | ||
|   | 0824aa0837 | ||
|   | 10b7db3057 | ||
|   | e2fc9e228e | ||
|   | d0eafd93ab | ||
|   | 6aea2ad31c | ||
|   | 6c98b16766 | ||
|   | 7e3ab368e2 | ||
|   | b60fa8a444 | ||
|   | 854112caeb | ||
|   | eebb90e43b | ||
|   | 9f708729a8 | ||
|   | 143b6f9403 | ||
|   | 76ee62519e | ||
|   | 75b9f474ab | ||
|   | dae6e51112 | 
							
								
								
									
										4
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file | ||||||
|  | # Ignore build and test binaries. | ||||||
|  | bin/ | ||||||
|  | testbin/ | ||||||
							
								
								
									
										36
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | --- | ||||||
|  | name: Bug report | ||||||
|  | about: Report bugs and errors found while using the Operator. | ||||||
|  | title: '' | ||||||
|  | labels: bug | ||||||
|  | assignees: '' | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### Your environment | ||||||
|  |  | ||||||
|  | <!-- Version of the Operator when the error occurred --> | ||||||
|  | Operator Version: | ||||||
|  |  | ||||||
|  | <!-- What version of the Connect server are you running? | ||||||
|  | You can get this information from the Integrations section in 1Password | ||||||
|  | https://start.1password.com/integrations/active | ||||||
|  | --> | ||||||
|  | Connect Server Version: | ||||||
|  |  | ||||||
|  | <!-- What version of Kubernetes have you deployed the operator to? --> | ||||||
|  | Kubernetes Version: | ||||||
|  |  | ||||||
|  | ## What happened? | ||||||
|  | <!-- Describe the bug or error --> | ||||||
|  |  | ||||||
|  | ## What did you expect to happen? | ||||||
|  | <!-- Describe what should have happened --> | ||||||
|  |  | ||||||
|  | ## Steps to reproduce | ||||||
|  | 1. <!-- Describe Steps to reproduce the issue --> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Notes & Logs | ||||||
|  | <!-- Paste any logs here that may help with debugging. | ||||||
|  | Remember to remove any sensitive information before sharing! --> | ||||||
							
								
								
									
										9
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | # 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 | ||||||
|  | blank_issues_enabled: true | ||||||
|  | contact_links: | ||||||
|  |   - name: 1Password Community | ||||||
|  |     url: https://1password.community/categories/secrets-automation | ||||||
|  |     about: Please ask general Secrets Automation questions here. | ||||||
|  |   - name: 1Password Security Bug Bounty | ||||||
|  |     url: https://bugcrowd.com/agilebits | ||||||
|  |     about: Please report security vulnerabilities here. | ||||||
							
								
								
									
										32
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | --- | ||||||
|  | name: Feature request | ||||||
|  | about: Suggest an idea for the Operator | ||||||
|  | title: '' | ||||||
|  | labels: feature-request | ||||||
|  | assignees: '' | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### Summary | ||||||
|  | <!-- Briefly describe the feature in one or two sentences. You can include more details later. --> | ||||||
|  |  | ||||||
|  | ### Use cases | ||||||
|  | <!-- Describe the use cases that make this feature useful to others. | ||||||
|  | The description should help the reader understand why the feature is necessary. | ||||||
|  | The better we understand your use case, the better we can help create an appropriate solution. --> | ||||||
|  |  | ||||||
|  | ### Proposed solution | ||||||
|  | <!-- If you already have an idea for how the feature should work, use this space to describe it. | ||||||
|  | We'll work with you to find a workable approach, and any implementation details are appreciated. | ||||||
|  | --> | ||||||
|  |  | ||||||
|  | ### Is there a workaround to accomplish this today? | ||||||
|  | <!-- If there's a way to accomplish this feature request without changes to the codebase, we'd like to hear it. | ||||||
|  | --> | ||||||
|  |  | ||||||
|  | ### References & Prior Work | ||||||
|  | <!-- If a similar feature was implemented in another project or tool, add a link so we can better understand your request. | ||||||
|  | Links to relevant documentation or RFCs are also appreciated. --> | ||||||
|  |  | ||||||
|  | * <!-- Reference 1 --> | ||||||
|  | * <!-- Reference 2, etc --> | ||||||
							
								
								
									
										21
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | name: Build and Test | ||||||
|  | on: [push, pull_request] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   build: | ||||||
|  |     name: Build | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |     - name: Set up Go 1.x | ||||||
|  |       uses: actions/setup-go@v2 | ||||||
|  |       with: | ||||||
|  |         go-version: ^1.15 | ||||||
|  |  | ||||||
|  |     - name: Check out code into the Go module directory | ||||||
|  |       uses: actions/checkout@v2 | ||||||
|  |  | ||||||
|  |     - name: Build | ||||||
|  |       run: go build -v ./... | ||||||
|  |  | ||||||
|  |     - name: Test | ||||||
|  |       run: go test -v ./... -cover | ||||||
							
								
								
									
										85
									
								
								.github/workflows/release-pr.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								.github/workflows/release-pr.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | on: | ||||||
|  |   create: | ||||||
|  |     branches: | ||||||
|  |  | ||||||
|  | name: Open Release PR for review | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   # This job is necessary because GitHub does not (yet) support | ||||||
|  |   # filtering `create` triggers by branch name. | ||||||
|  |   # See: https://github.community/t/trigger-job-on-branch-created/16878/5 | ||||||
|  |   should_create_pr: | ||||||
|  |     name: Check if PR for branch already exists | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     outputs: | ||||||
|  |       result: ${{ steps.is_release_branch_without_pr.outputs.result }} | ||||||
|  |     steps: | ||||||
|  |       - id: is_release_branch_without_pr | ||||||
|  |         name: Find matching PR | ||||||
|  |         uses: actions/github-script@v3 | ||||||
|  |         with: | ||||||
|  |           github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |           script: | | ||||||
|  |             // Search for an existing PR with head & base | ||||||
|  |             //  that match the created branch | ||||||
|  |  | ||||||
|  |             const [releaseBranchName] = context.ref.match("release/v[0-9]+\.[0-9]+\.[0-9]+") || [] | ||||||
|  |  | ||||||
|  |             if(!releaseBranchName) { return false } | ||||||
|  |  | ||||||
|  |             const {data: prs} = await github.pulls.list({ | ||||||
|  |                 ...context.repo, | ||||||
|  |                 state: 'open', | ||||||
|  |                 head: `1Password:${releaseBranchName}`, | ||||||
|  |                 base: context.payload.master_branch | ||||||
|  |             }) | ||||||
|  |  | ||||||
|  |             return prs.length === 0 | ||||||
|  |  | ||||||
|  |   create_pr: | ||||||
|  |     needs: should_create_pr | ||||||
|  |     if: needs.should_create_pr.outputs.result == 'true' | ||||||
|  |     name: Create Release Pull Request | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |  | ||||||
|  |       - name: Parse release version | ||||||
|  |         id: get_version | ||||||
|  |         run: echo "::set-output name=version::$(echo $GITHUB_REF | sed 's|^refs/heads/release/v?*||g')" | ||||||
|  |  | ||||||
|  |       - name: Prepare Pull Request | ||||||
|  |         id: prep_pr | ||||||
|  |         run: | | ||||||
|  |           CHANGELOG_PATH=$(printf "%s/CHANGELOG.md" "${GITHUB_WORKSPACE}") | ||||||
|  |  | ||||||
|  |           LOG_ENTRY=$(awk '/START\/v[0-9]+\.[0-9]+\.[0-9]+*/{f=1; next} /---/{if (f == 1) exit} f' "${CHANGELOG_PATH}") | ||||||
|  |           export PR_BODY=$(cat <<EOF | ||||||
|  |           This is an automated PR for a new release. | ||||||
|  |  | ||||||
|  |           Please check the following before approving: | ||||||
|  |           - [ ] Changelog is accurate. The documented changes for this release are printed below. | ||||||
|  |           - [ ] Any files referencing a version number. Confirm it matches the version number in the branch name. | ||||||
|  |           --- | ||||||
|  |           ## Release Changelog Preview | ||||||
|  |           ${LOG_ENTRY} | ||||||
|  |           EOF | ||||||
|  |           ) | ||||||
|  |  | ||||||
|  |           # Sanitizes multiline strings for action outputs (https://medium.com/agorapulse-stories/23f56447d209) | ||||||
|  |           PR_BODY="${PR_BODY//'%'/'%25'}" | ||||||
|  |           PR_BODY="${PR_BODY//$'\n'/'%0A'}" | ||||||
|  |           PR_BODY="${PR_BODY//$'\r'/'%0D'}" | ||||||
|  |           echo "::set-output name=pr_body::$(echo "$PR_BODY")" | ||||||
|  |  | ||||||
|  |       - name: Create Pull Request via API | ||||||
|  |         id: post_pr | ||||||
|  |         uses: octokit/request-action@v2.x | ||||||
|  |         with: | ||||||
|  |           route: POST /repos/${{ github.repository }}/pulls | ||||||
|  |           title: ${{ format('Prepare Release - v{0}', steps.get_version.outputs.version) }} | ||||||
|  |           head: ${{ github.ref }} | ||||||
|  |           base: ${{ github.event.master_branch }} | ||||||
|  |           body: ${{ toJson(steps.prep_pr.outputs.pr_body) }} | ||||||
|  |         env: | ||||||
|  |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
							
								
								
									
										57
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | name: release | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     tags: | ||||||
|  |       - 'v*' | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   release-docker: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     env: | ||||||
|  |       DOCKER_CLI_EXPERIMENTAL: "enabled" | ||||||
|  |     steps: | ||||||
|  |       - | ||||||
|  |         name: Checkout | ||||||
|  |         uses: actions/checkout@v2 | ||||||
|  |         with: | ||||||
|  |           fetch-depth: 0 | ||||||
|  |       - | ||||||
|  |         name: Docker meta | ||||||
|  |         id: meta | ||||||
|  |         uses: crazy-max/ghaction-docker-meta@v2 | ||||||
|  |         with: | ||||||
|  |           images: | | ||||||
|  |             1password/onepassword-operator | ||||||
|  |           # Publish image for x.y.z and x.y | ||||||
|  |           # The latest tag is automatically added for semver tags | ||||||
|  |           tags: | | ||||||
|  |             type=semver,pattern={{version}} | ||||||
|  |             type=semver,pattern={{major}}.{{minor}} | ||||||
|  |       - name: Get the version from tag | ||||||
|  |         id: get_version | ||||||
|  |         run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} | ||||||
|  |       - | ||||||
|  |         name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v1 | ||||||
|  |       - | ||||||
|  |         name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v1 | ||||||
|  |       - | ||||||
|  |         name: Docker Login | ||||||
|  |         uses: docker/login-action@v1 | ||||||
|  |         with: | ||||||
|  |           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||||
|  |           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||
|  |       - | ||||||
|  |         name: Build and push | ||||||
|  |         uses: docker/build-push-action@v2 | ||||||
|  |         with: | ||||||
|  |           context: . | ||||||
|  |           file: Dockerfile | ||||||
|  |           platforms: linux/amd64,linux/arm64,linux/arm/v7 | ||||||
|  |           push: true | ||||||
|  |           tags: ${{ steps.meta.outputs.tags }} | ||||||
|  |           labels: ${{ steps.meta.outputs.labels }} | ||||||
|  |           build-args: | | ||||||
|  |             operator_version=${{ steps.get_version.outputs.VERSION }} | ||||||
							
								
								
									
										87
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										87
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,80 +1,25 @@ | |||||||
| # Temporary Build Files |  | ||||||
| build/_output |  | ||||||
| build/_test |  | ||||||
| # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode |  | ||||||
| ### Emacs ### |  | ||||||
| # -*- mode: gitignore; -*- |  | ||||||
| *~ |  | ||||||
| \#*\# |  | ||||||
| /.emacs.desktop |  | ||||||
| /.emacs.desktop.lock |  | ||||||
| *.elc |  | ||||||
| auto-save-list |  | ||||||
| tramp |  | ||||||
| .\#* |  | ||||||
| # Org-mode |  | ||||||
| .org-id-locations |  | ||||||
| *_archive |  | ||||||
| # flymake-mode |  | ||||||
| *_flymake.* |  | ||||||
| # eshell files |  | ||||||
| /eshell/history |  | ||||||
| /eshell/lastdir |  | ||||||
| # elpa packages |  | ||||||
| /elpa/ |  | ||||||
| # reftex files |  | ||||||
| *.rel |  | ||||||
| # AUCTeX auto folder |  | ||||||
| /auto/ |  | ||||||
| # cask packages |  | ||||||
| .cask/ |  | ||||||
| dist/ |  | ||||||
| # Flycheck |  | ||||||
| flycheck_*.el |  | ||||||
| # server auth directory |  | ||||||
| /server/ |  | ||||||
| # projectiles files |  | ||||||
| .projectile |  | ||||||
| projectile-bookmarks.eld |  | ||||||
| # directory configuration |  | ||||||
| .dir-locals.el |  | ||||||
| # saveplace |  | ||||||
| places |  | ||||||
| # url cache |  | ||||||
| url/cache/ |  | ||||||
| # cedet |  | ||||||
| ede-projects.el |  | ||||||
| # smex |  | ||||||
| smex-items |  | ||||||
| # company-statistics |  | ||||||
| company-statistics-cache.el |  | ||||||
| # anaconda-mode |  | ||||||
| anaconda-mode/ |  | ||||||
| ### Go ### |  | ||||||
| # Binaries for programs and plugins | # Binaries for programs and plugins | ||||||
| *.exe | *.exe | ||||||
| *.exe~ | *.exe~ | ||||||
| *.dll | *.dll | ||||||
| *.so | *.so | ||||||
| *.dylib | *.dylib | ||||||
| # Test binary, build with 'go test -c' | bin | ||||||
|  | testbin/* | ||||||
|  |  | ||||||
|  | # Test binary, build with `go test -c` | ||||||
| *.test | *.test | ||||||
|  |  | ||||||
| # Output of the go coverage tool, specifically when used with LiteIDE | # Output of the go coverage tool, specifically when used with LiteIDE | ||||||
| *.out | *.out | ||||||
| ### Vim ### |  | ||||||
| # swap | # Kubernetes Generated files - skip generated files, except for vendored files | ||||||
| .sw[a-p] |  | ||||||
| .*.sw[a-p] | !vendor/**/zz_generated.* | ||||||
| # session |  | ||||||
| Session.vim | # editor and IDE paraphernalia | ||||||
| # temporary | .idea | ||||||
| .netrwhist | *.swp | ||||||
| # auto-generated tag files | *.swo | ||||||
| tags | *~ | ||||||
| ### VisualStudioCode ### |  | ||||||
| .vscode/* |  | ||||||
| .history |  | ||||||
| .DS_Store |  | ||||||
| op-ss-client/ |  | ||||||
| .idea/ |  | ||||||
| # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode |  | ||||||
|   | |||||||
							
								
								
									
										135
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | [//]: # (START/LATEST) | ||||||
|  | # Latest | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |   * A user-friendly description of a new feature. {issue-number} | ||||||
|  |  | ||||||
|  | ## Fixes | ||||||
|  |  * A user-friendly description of a fix. {issue-number} | ||||||
|  |  | ||||||
|  | ## Security | ||||||
|  |  * A user-friendly description of a security fix. {issue-number} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v1.4.1" | ||||||
|  |  | ||||||
|  | # v1.4.1 | ||||||
|  |  | ||||||
|  | ## Fixes | ||||||
|  |  | ||||||
|  | - OwnerReferences on secrets are now persisted after an item is updated. {#101} | ||||||
|  | - Annotations from a Deployment or OnePasswordItem are no longer applied to Secrets that are created for it. {#102} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v1.4.0" | ||||||
|  |  | ||||||
|  | # v1.4.0 | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - 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} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v1.3.0" | ||||||
|  |  | ||||||
|  | # v1.3.0 | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - Added support for loading secrets from files stored in 1Password. {#47} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v1.2.0" | ||||||
|  |  | ||||||
|  | # v1.2.0 | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - Support secrets provisioned through FromEnv. {#74} | ||||||
|  | - Support configuration of Kubernetes Secret type. {#87} | ||||||
|  | - Improved logging. (#72) | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v1.1.0" | ||||||
|  |  | ||||||
|  | # v1.1.0 | ||||||
|  |  | ||||||
|  | ## Fixes | ||||||
|  |  | ||||||
|  | - Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v1.0.2" | ||||||
|  |  | ||||||
|  | # v1.0.2 | ||||||
|  |  | ||||||
|  | ## Fixes | ||||||
|  |  | ||||||
|  | - Name normalizer added to handle non-conforming item names. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v1.0.1" | ||||||
|  |  | ||||||
|  | # v1.0.1 | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - This release also contains an arm64 Docker image. {#20} | ||||||
|  | - Docker images are also pushed to the :latest and :<major>.<minor> tags. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v1.0.0" | ||||||
|  |  | ||||||
|  | # v1.0.0 | ||||||
|  |  | ||||||
|  | ## Features: | ||||||
|  |  | ||||||
|  | - Option to automatically deploy 1Password Connect via the operator | ||||||
|  | - Ignore restart annotation when looking for 1Password annotations | ||||||
|  | - Release Automation | ||||||
|  | - Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource | ||||||
|  | - Adding configuration for auto rolling restart on deployments | ||||||
|  | - Configure Auto Restarts for a OnePasswordItem Custom Resource | ||||||
|  | - Update Connect Dependencies to latest | ||||||
|  | - Add Github action for building and testing operator | ||||||
|  |  | ||||||
|  | ## Fixes: | ||||||
|  |  | ||||||
|  | - Fix spec field example for OnePasswordItem in readme | ||||||
|  | - Casing of annotations are now consistent | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v0.0.2" | ||||||
|  |  | ||||||
|  | # v0.0.2 | ||||||
|  |  | ||||||
|  | ## Features: | ||||||
|  |  | ||||||
|  | - Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>` | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # "START/v0.0.1" | ||||||
|  |  | ||||||
|  | # v0.0.1 | ||||||
|  |  | ||||||
|  | Initial 1Password Operator release | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret | ||||||
|  | - watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets | ||||||
|  | - watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes | ||||||
|  | - restart pods when secret has been updated | ||||||
|  | - cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted | ||||||
|  |  | ||||||
|  | --- | ||||||
							
								
								
									
										19
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,24 +1,27 @@ | |||||||
| # Build the manager binary | # Build the manager binary | ||||||
| FROM golang:1.13 as builder | FROM golang:1.17 as builder | ||||||
|  |  | ||||||
| WORKDIR /workspace | WORKDIR /workspace | ||||||
| # Copy the Go Modules manifests | # Copy the Go Modules manifests | ||||||
| COPY go.mod go.mod | COPY go.mod go.mod | ||||||
| COPY go.sum go.sum | COPY go.sum go.sum | ||||||
|  | # cache deps before building and copying source so that we don't need to re-download as much | ||||||
|  | # and so that source changes don't invalidate our downloaded layer | ||||||
|  | RUN go mod download | ||||||
|  |  | ||||||
| # Copy the go source | # Copy the go source | ||||||
| COPY cmd/manager/main.go main.go | COPY main.go main.go | ||||||
| COPY pkg/ pkg/ | COPY api/ api/ | ||||||
| COPY version/ version/ | COPY controllers/ controllers/ | ||||||
| COPY vendor/ vendor/ |  | ||||||
| # Build | # Build | ||||||
| RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -mod vendor -a -o manager main.go | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go | ||||||
|  |  | ||||||
| # Use distroless as minimal base image to package the manager binary | # Use distroless as minimal base image to package the manager binary | ||||||
| # Refer to https://github.com/GoogleContainerTools/distroless for more details | # Refer to https://github.com/GoogleContainerTools/distroless for more details | ||||||
| FROM gcr.io/distroless/static:nonroot | FROM gcr.io/distroless/static:nonroot | ||||||
| WORKDIR / | WORKDIR / | ||||||
| COPY --from=builder /workspace/manager . | COPY --from=builder /workspace/manager . | ||||||
| USER nonroot:nonroot | USER 65532:65532 | ||||||
|  |  | ||||||
| ENTRYPOINT ["/manager"] | ENTRYPOINT ["/manager"] | ||||||
|   | |||||||
							
								
								
									
										233
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,233 @@ | |||||||
|  | # VERSION defines the project version for the bundle. | ||||||
|  | # Update this value when you upgrade the version of your project. | ||||||
|  | # To re-generate a bundle for another specific version without changing the standard setup, you can: | ||||||
|  | # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) | ||||||
|  | # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) | ||||||
|  | VERSION ?= 0.0.1 | ||||||
|  |  | ||||||
|  | # CHANNELS define the bundle channels used in the bundle. | ||||||
|  | # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") | ||||||
|  | # To re-generate a bundle for other specific channels without changing the standard setup, you can: | ||||||
|  | # - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) | ||||||
|  | # - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") | ||||||
|  | ifneq ($(origin CHANNELS), undefined) | ||||||
|  | BUNDLE_CHANNELS := --channels=$(CHANNELS) | ||||||
|  | endif | ||||||
|  |  | ||||||
|  | # DEFAULT_CHANNEL defines the default channel used in the bundle. | ||||||
|  | # Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") | ||||||
|  | # To re-generate a bundle for any other default channel without changing the default setup, you can: | ||||||
|  | # - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) | ||||||
|  | # - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") | ||||||
|  | ifneq ($(origin DEFAULT_CHANNEL), undefined) | ||||||
|  | BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) | ||||||
|  | endif | ||||||
|  | BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) | ||||||
|  |  | ||||||
|  | # IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. | ||||||
|  | # This variable is used to construct full image tags for bundle and catalog images. | ||||||
|  | # | ||||||
|  | # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both | ||||||
|  | # onepassword.com/onepassword-operator-new-bundle:$VERSION and onepassword.com/onepassword-operator-new-catalog:$VERSION. | ||||||
|  | IMAGE_TAG_BASE ?= onepassword.com/onepassword-operator-new | ||||||
|  |  | ||||||
|  | # BUNDLE_IMG defines the image:tag used for the bundle. | ||||||
|  | # You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>) | ||||||
|  | BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) | ||||||
|  |  | ||||||
|  | # BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command | ||||||
|  | BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) | ||||||
|  |  | ||||||
|  | # USE_IMAGE_DIGESTS defines if images are resolved via tags or digests | ||||||
|  | # You can enable this value if you would like to use SHA Based Digests | ||||||
|  | # To enable set flag to true | ||||||
|  | USE_IMAGE_DIGESTS ?= false | ||||||
|  | ifeq ($(USE_IMAGE_DIGESTS), true) | ||||||
|  | 	BUNDLE_GEN_FLAGS += --use-image-digests | ||||||
|  | endif | ||||||
|  |  | ||||||
|  | # Image URL to use all building/pushing image targets | ||||||
|  | IMG ?= controller:latest | ||||||
|  | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. | ||||||
|  | ENVTEST_K8S_VERSION = 1.23 | ||||||
|  |  | ||||||
|  | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) | ||||||
|  | ifeq (,$(shell go env GOBIN)) | ||||||
|  | GOBIN=$(shell go env GOPATH)/bin | ||||||
|  | else | ||||||
|  | GOBIN=$(shell go env GOBIN) | ||||||
|  | endif | ||||||
|  |  | ||||||
|  | # Setting SHELL to bash allows bash commands to be executed by recipes. | ||||||
|  | # This is a requirement for 'setup-envtest.sh' in the test target. | ||||||
|  | # Options are set to exit when a recipe line exits non-zero or a piped command fails. | ||||||
|  | SHELL = /usr/bin/env bash -o pipefail | ||||||
|  | .SHELLFLAGS = -ec | ||||||
|  |  | ||||||
|  | .PHONY: all | ||||||
|  | all: build | ||||||
|  |  | ||||||
|  | ##@ General | ||||||
|  |  | ||||||
|  | # The help target prints out all targets with their descriptions organized | ||||||
|  | # beneath their categories. The categories are represented by '##@' and the | ||||||
|  | # target descriptions by '##'. The awk commands is responsible for reading the | ||||||
|  | # entire set of makefiles included in this invocation, looking for lines of the | ||||||
|  | # file as xyz: ## something, and then pretty-format the target and help. Then, | ||||||
|  | # if there's a line with ##@ something, that gets pretty-printed as a category. | ||||||
|  | # More info on the usage of ANSI control characters for terminal formatting: | ||||||
|  | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters | ||||||
|  | # More info on the awk command: | ||||||
|  | # http://linuxcommand.org/lc3_adv_awk.php | ||||||
|  |  | ||||||
|  | .PHONY: help | ||||||
|  | help: ## Display this help. | ||||||
|  | 	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) | ||||||
|  |  | ||||||
|  | ##@ Development | ||||||
|  |  | ||||||
|  | .PHONY: manifests | ||||||
|  | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. | ||||||
|  | 	$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases | ||||||
|  |  | ||||||
|  | .PHONY: generate | ||||||
|  | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. | ||||||
|  | 	$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." | ||||||
|  |  | ||||||
|  | .PHONY: fmt | ||||||
|  | fmt: ## Run go fmt against code. | ||||||
|  | 	go fmt ./... | ||||||
|  |  | ||||||
|  | .PHONY: vet | ||||||
|  | vet: ## Run go vet against code. | ||||||
|  | 	go vet ./... | ||||||
|  |  | ||||||
|  | .PHONY: test | ||||||
|  | test: manifests generate fmt vet envtest ## Run tests. | ||||||
|  | 	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out | ||||||
|  |  | ||||||
|  | ##@ Build | ||||||
|  |  | ||||||
|  | .PHONY: build | ||||||
|  | build: generate fmt vet ## Build manager binary. | ||||||
|  | 	go build -o bin/manager main.go | ||||||
|  |  | ||||||
|  | .PHONY: run | ||||||
|  | run: manifests generate fmt vet ## Run a controller from your host. | ||||||
|  | 	go run ./main.go | ||||||
|  |  | ||||||
|  | .PHONY: docker-build | ||||||
|  | docker-build: test ## Build docker image with the manager. | ||||||
|  | 	docker build -t ${IMG} . | ||||||
|  |  | ||||||
|  | .PHONY: docker-push | ||||||
|  | docker-push: ## Push docker image with the manager. | ||||||
|  | 	docker push ${IMG} | ||||||
|  |  | ||||||
|  | ##@ Deployment | ||||||
|  |  | ||||||
|  | ifndef ignore-not-found | ||||||
|  |   ignore-not-found = false | ||||||
|  | endif | ||||||
|  |  | ||||||
|  | .PHONY: install | ||||||
|  | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. | ||||||
|  | 	$(KUSTOMIZE) build config/crd | kubectl apply -f - | ||||||
|  |  | ||||||
|  | .PHONY: uninstall | ||||||
|  | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. | ||||||
|  | 	$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - | ||||||
|  |  | ||||||
|  | .PHONY: deploy | ||||||
|  | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. | ||||||
|  | 	cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} | ||||||
|  | 	$(KUSTOMIZE) build config/default | kubectl apply -f - | ||||||
|  |  | ||||||
|  | .PHONY: undeploy | ||||||
|  | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. | ||||||
|  | 	$(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - | ||||||
|  |  | ||||||
|  | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen | ||||||
|  | .PHONY: controller-gen | ||||||
|  | controller-gen: ## Download controller-gen locally if necessary. | ||||||
|  | 	$(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0) | ||||||
|  |  | ||||||
|  | KUSTOMIZE = $(shell pwd)/bin/kustomize | ||||||
|  | .PHONY: kustomize | ||||||
|  | kustomize: ## Download kustomize locally if necessary. | ||||||
|  | 	$(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) | ||||||
|  |  | ||||||
|  | ENVTEST = $(shell pwd)/bin/setup-envtest | ||||||
|  | .PHONY: envtest | ||||||
|  | envtest: ## Download envtest-setup locally if necessary. | ||||||
|  | 	$(call go-get-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) | ||||||
|  |  | ||||||
|  | # go-get-tool will 'go get' any package $2 and install it to $1. | ||||||
|  | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) | ||||||
|  | define go-get-tool | ||||||
|  | @[ -f $(1) ] || { \ | ||||||
|  | set -e ;\ | ||||||
|  | TMP_DIR=$$(mktemp -d) ;\ | ||||||
|  | cd $$TMP_DIR ;\ | ||||||
|  | go mod init tmp ;\ | ||||||
|  | echo "Downloading $(2)" ;\ | ||||||
|  | GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ | ||||||
|  | rm -rf $$TMP_DIR ;\ | ||||||
|  | } | ||||||
|  | endef | ||||||
|  |  | ||||||
|  | .PHONY: bundle | ||||||
|  | bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files. | ||||||
|  | 	operator-sdk generate kustomize manifests -q | ||||||
|  | 	cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) | ||||||
|  | 	$(KUSTOMIZE) build config/manifests | operator-sdk generate bundle $(BUNDLE_GEN_FLAGS) | ||||||
|  | 	operator-sdk bundle validate ./bundle | ||||||
|  |  | ||||||
|  | .PHONY: bundle-build | ||||||
|  | bundle-build: ## Build the bundle image. | ||||||
|  | 	docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . | ||||||
|  |  | ||||||
|  | .PHONY: bundle-push | ||||||
|  | bundle-push: ## Push the bundle image. | ||||||
|  | 	$(MAKE) docker-push IMG=$(BUNDLE_IMG) | ||||||
|  |  | ||||||
|  | .PHONY: opm | ||||||
|  | OPM = ./bin/opm | ||||||
|  | opm: ## Download opm locally if necessary. | ||||||
|  | ifeq (,$(wildcard $(OPM))) | ||||||
|  | ifeq (,$(shell which opm 2>/dev/null)) | ||||||
|  | 	@{ \ | ||||||
|  | 	set -e ;\ | ||||||
|  | 	mkdir -p $(dir $(OPM)) ;\ | ||||||
|  | 	OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ | ||||||
|  | 	curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.19.1/$${OS}-$${ARCH}-opm ;\ | ||||||
|  | 	chmod +x $(OPM) ;\ | ||||||
|  | 	} | ||||||
|  | else | ||||||
|  | OPM = $(shell which opm) | ||||||
|  | endif | ||||||
|  | endif | ||||||
|  |  | ||||||
|  | # A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). | ||||||
|  | # These images MUST exist in a registry and be pull-able. | ||||||
|  | BUNDLE_IMGS ?= $(BUNDLE_IMG) | ||||||
|  |  | ||||||
|  | # The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). | ||||||
|  | CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) | ||||||
|  |  | ||||||
|  | # Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. | ||||||
|  | ifneq ($(origin CATALOG_BASE_IMG), undefined) | ||||||
|  | FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) | ||||||
|  | endif | ||||||
|  |  | ||||||
|  | # Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. | ||||||
|  | # This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: | ||||||
|  | # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator | ||||||
|  | .PHONY: catalog-build | ||||||
|  | catalog-build: opm ## Build a catalog image. | ||||||
|  | 	$(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) | ||||||
|  |  | ||||||
|  | # Push the catalog image. | ||||||
|  | .PHONY: catalog-push | ||||||
|  | catalog-push: ## Push a catalog image. | ||||||
|  | 	$(MAKE) docker-push IMG=$(CATALOG_IMG) | ||||||
							
								
								
									
										19
									
								
								PROJECT
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								PROJECT
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | domain: onepassword.com | ||||||
|  | layout: | ||||||
|  | - go.kubebuilder.io/v3 | ||||||
|  | plugins: | ||||||
|  |   manifests.sdk.operatorframework.io/v2: {} | ||||||
|  |   scorecard.sdk.operatorframework.io/v2: {} | ||||||
|  | projectName: onepassword-operator-new | ||||||
|  | repo: github.com/1Password/onepassword-operator | ||||||
|  | resources: | ||||||
|  | - api: | ||||||
|  |     crdVersion: v1 | ||||||
|  |     namespaced: true | ||||||
|  |   controller: true | ||||||
|  |   domain: onepassword.com | ||||||
|  |   group: onepassword | ||||||
|  |   kind: OnePasswordItem | ||||||
|  |   path: github.com/1Password/onepassword-operator/api/v1 | ||||||
|  |   version: v1 | ||||||
|  | version: "3" | ||||||
							
								
								
									
										184
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										184
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,10 +1,12 @@ | |||||||
|  | // TODO: Update README.md | ||||||
|  |  | ||||||
| # 1Password Connect Kubernetes Operator | # 1Password Connect Kubernetes Operator | ||||||
|  |  | ||||||
| The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item. | The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item. | ||||||
|  |  | ||||||
| The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment. | The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment. | ||||||
|  |  | ||||||
| The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret will be automatically restarted. | The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted. | ||||||
|  |  | ||||||
| ## Setup | ## Setup | ||||||
|  |  | ||||||
| @@ -13,53 +15,81 @@ Prerequisites: | |||||||
| - [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/) | - [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/) | ||||||
| - [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/) | - [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/) | ||||||
| - [docker installed](https://docs.docker.com/get-docker/) | - [docker installed](https://docs.docker.com/get-docker/) | ||||||
| - [1Password Connect has been setup with an API token issued to be used with the operator.](https://support.b5dev.com/cs/connect) | - [Generated a 1password-credentials.json file and issued a 1Password Connect API Token for the K8s Operator integration](https://support.1password.com/secrets-automation/) | ||||||
| - [1Password Connect deployed to Kubernetes](https://support.b5dev.com/cs/connect) | - [1Password Connect deployed to Kubernetes](https://support.1password.com/connect-deploy-kubernetes/#step-2-deploy-a-1password-connect-server). **NOTE**: If customization of the 1Password Connect deployment is not required you can skip this prerequisite. | ||||||
|  |  | ||||||
|  | ### Quickstart for Deploying 1Password Connect to Kubernetes | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #### Deploy with Helm | ||||||
|  | The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes.  | ||||||
|  |  | ||||||
|  | [The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts) | ||||||
|  |  | ||||||
|  | #### Deploy using the Connect Operator | ||||||
|  | If 1Password Connect is already running, you can skip this step. 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. | ||||||
|  |  | ||||||
|  | Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | cat 1password-credentials.json | base64 | \ | ||||||
|  |   tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Create a Kubernetes secret from the op-session file: | ||||||
|  | ```bash | ||||||
|  | kubectl create secret generic op-credentials --from-file=op-session | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`: | ||||||
|  | ```yaml | ||||||
|  | - name: MANAGE_CONNECT | ||||||
|  |   value: "true" | ||||||
|  | ``` | ||||||
|  | Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the `default` namespace. | ||||||
| ### Kubernetes Operator Deployment | ### Kubernetes Operator Deployment | ||||||
|  |  | ||||||
| **Create Kubernetes Secret for OP_CONNECT_TOKEN** | **Create Kubernetes Secret for OP_CONNECT_TOKEN** | ||||||
|  |  | ||||||
|  | "Create a Connect token for the operator and save it as a Kubernetes Secret:  | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| # where <OP_CONNECT_TOKEN> is the 1Password Connect API token | kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>" | ||||||
| $ kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN> |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command: | ||||||
|  | ```bash | ||||||
|  | kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | [More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens) | ||||||
|  |  | ||||||
| **Set Permissions For Operator** | **Set Permissions For Operator** | ||||||
|  |  | ||||||
| We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder. | We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder. | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl apply -f deploy/permissions.yaml | kubectl apply -f deploy/permissions.yaml | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| **Create Custom One Password Secret Resource** | **Create Custom One Password Secret Resource** | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml | kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| **Deploying the Operator** | **Deploying the Operator** | ||||||
|  |  | ||||||
| An example Deployment yaml can be found at `/deploy/operator.yaml`. | An sample Deployment yaml can be found at `/deploy/operator.yaml`. | ||||||
|  |  | ||||||
| ```yaml |  | ||||||
| containers: |  | ||||||
|     - name: onepassword-operator |  | ||||||
|       image: 1password/onepassword-operator |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| and update the image pull policy to `Always` | To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml: | ||||||
|  |  | ||||||
| ```yaml |  | ||||||
| imagePullPolicy: Always |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the deployment yaml: |  | ||||||
|  |  | ||||||
| - **WATCH_NAMESPACE:** comma separated list of what Namespaces to watch for changes. |  | ||||||
| - **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect. | - **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect. | ||||||
| - **POLLING_INTERVAL** (default: 600)**:** The number of seconds ****the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. | - **WATCH_NAMESPACE:** (default: watch all namespaces): Comma separated list of what Namespaces to watch for changes. | ||||||
|  | - **POLLING_INTERVAL** (default: 600): The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. | ||||||
|  | - **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 `default` namespace. | ||||||
|  | - **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. | ||||||
|  |  | ||||||
| Apply the deployment file: | Apply the deployment file: | ||||||
|  |  | ||||||
| @@ -73,25 +103,23 @@ To create a Kubernetes Secret from a 1Password item, create a yaml file with the | |||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| apiVersion: onepassword.com/v1 | apiVersion: onepassword.com/v1 | ||||||
| kind: OnePasswordItem # {insert_new_name} | kind: OnePasswordItem | ||||||
| metadata: | metadata: | ||||||
|   name: {item_name} #this name will also be used for naming the generated kubernetes secret |   name: <item_name> #this name will also be used for naming the generated kubernetes secret | ||||||
| spec: | spec: | ||||||
|   item-path: "vaults/{vaultId}/items/{itemId}"  |   itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"  | ||||||
| # where vaultId is the id of the vault in which to find the item |  | ||||||
| # where itemId is the id of the item that you want to store as a Kubernetes Secret |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Deploy the OnePasswordItem to Kubernetes: | Deploy the OnePasswordItem to Kubernetes: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl apply -f {your_item}.yaml | kubectl apply -f <your_item>.yaml | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| To test that the Kubernetes Secret check that the following command returns a secret: | To test that the Kubernetes Secret check that the following command returns a secret: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl get secret {secret_name} | kubectl get secret <secret_name> | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret. | Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret. | ||||||
| @@ -104,22 +132,104 @@ kind: Deployment | |||||||
| metadata: | metadata: | ||||||
|   name: deployment-example |   name: deployment-example | ||||||
|   annotations: |   annotations: | ||||||
|     onepasswordoperator/item-path: "vaults/{vaultId}/items/{itemId}" |     operator.1password.io/item-path: "vaults/<vault_id_or_title>/items/<item_id_or_title>" | ||||||
|     onepasswordoperator/item-name: "{secret_name}" |     operator.1password.io/item-name: "<secret_name>" | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Applying this yaml file will create a Kubernetes Secret with the name `{secret_name}` and contents from the location specified at the specified Item Path. | Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Path. | ||||||
|  |  | ||||||
| Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `onepasswordoperator./item-path` and `onepasswordoperator/item-name` and no other deployment is using the secret. | 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.  | ||||||
|  | In case of fields that store files, the file's contents will be used as the value. | ||||||
|  |  | ||||||
| If a 1Password Item that is linked to a Kubernetes Secret is updated within the `POLLING_INTERVAL` the associated Kubernetes Secret will be updated. Furthermore, any deployments using that secret will be given a rolling restart. | 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. | ||||||
|  |  | ||||||
|  | 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. | ||||||
|  |  | ||||||
|  | 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. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  | **NOTE** | ||||||
|  |  | ||||||
|  | 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.  | ||||||
|  |  | ||||||
|  | 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: | ||||||
|  |  - Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed | ||||||
|  |  - All whitespaces between words will be replaced by `-` | ||||||
|  |  - All the letters will be lower-cased. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### Configuring Automatic Rolling Restarts of Deployments | ||||||
|  |  | ||||||
|  | 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. | ||||||
|  |  | ||||||
|  | There are many levels of granularity on which to configure auto restarts on deployments: at the operator level, per-namespace, or per-deployment. | ||||||
|  |  | ||||||
|  | **On the operator**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environemnt variable  `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false. | ||||||
|  |  | ||||||
|  | **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: | ||||||
|  | ```yaml | ||||||
|  | # enabled auto restarts for all deployments within a namespace unless overwritten within a deployment | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: Namespace | ||||||
|  | metadata: | ||||||
|  |   name: "example-namespace" | ||||||
|  |   annotations: | ||||||
|  |     operator.1password.io/auto-restart: "true" | ||||||
|  | ``` | ||||||
|  | If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment. | ||||||
|  |  | ||||||
|  | **Per Deployment** | ||||||
|  | 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: | ||||||
|  | ```yaml | ||||||
|  | # enabled auto restarts for the deployment | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: Deployment | ||||||
|  | metadata: | ||||||
|  |   name: "example-deployment" | ||||||
|  |   annotations: | ||||||
|  |     operator.1password.io/auto-restart: "true" | ||||||
|  | ``` | ||||||
|  | If the value is not set, the auto restart settings on the namespace will be used. | ||||||
|  |  | ||||||
|  | **Per OnePasswordItem Custom Resource** | ||||||
|  | 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: | ||||||
|  | ```yaml | ||||||
|  | # enabled auto restarts for the OnePasswordItem | ||||||
|  | apiVersion: onepassword.com/v1 | ||||||
|  | kind: OnePasswordItem | ||||||
|  | metadata: | ||||||
|  |   name: example | ||||||
|  |   annotations: | ||||||
|  |     operator.1password.io/auto-restart: "true" | ||||||
|  | ``` | ||||||
|  | If the value is not set, the auto restart settings on the deployment will be used. | ||||||
|  |  | ||||||
| ## Development | ## Development | ||||||
|  |  | ||||||
|  | ### Creating a Docker image | ||||||
|  |  | ||||||
|  | To create a local version of the Docker image for testing, use the following `Makefile` target: | ||||||
|  | ```shell | ||||||
|  | make build/local | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Building the Operator binary | ||||||
|  | ```shell | ||||||
|  | make build/binary | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The binary will be placed inside a `dist` folder within this repository. | ||||||
|  |  | ||||||
| ### Running Tests | ### Running Tests | ||||||
|  |  | ||||||
| ```bash | ```shell | ||||||
| $ go test -v ./... -cover | make test | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | With coverage: | ||||||
|  | ```shell | ||||||
|  | make test/coverage | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Security | ## Security | ||||||
| @@ -128,4 +238,4 @@ $ go test -v ./... -cover | |||||||
|  |  | ||||||
| Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).  | Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).  | ||||||
|  |  | ||||||
| For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits). | For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits). | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								api/v1/groupversion_info.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								api/v1/groupversion_info.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | // Package v1 contains API Schema definitions for the onepassword v1 API group | ||||||
|  | //+kubebuilder:object:generate=true | ||||||
|  | //+groupName=onepassword.onepassword.com | ||||||
|  | package v1 | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/scheme" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// GroupVersion is group version used to register these objects | ||||||
|  | 	GroupVersion = schema.GroupVersion{Group: "onepassword.onepassword.com", Version: "v1"} | ||||||
|  |  | ||||||
|  | 	// SchemeBuilder is used to add go types to the GroupVersionKind scheme | ||||||
|  | 	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} | ||||||
|  |  | ||||||
|  | 	// AddToScheme adds the types in this group-version to the given scheme. | ||||||
|  | 	AddToScheme = SchemeBuilder.AddToScheme | ||||||
|  | ) | ||||||
| @@ -1,37 +1,57 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022. | ||||||
|  | 
 | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  | 
 | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | 
 | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
| package v1 | package v1 | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN! | ||||||
| // NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized. | // NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized. | ||||||
| 
 | 
 | ||||||
| // OnePasswordItemSpec defines the desired state of OnePasswordItem | // OnePasswordItemSpec defines the desired state of OnePasswordItem | ||||||
| type OnePasswordItemSpec struct { | type OnePasswordItemSpec struct { | ||||||
|  | 	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster | ||||||
|  | 	// Important: Run "make" to regenerate code after modifying this file | ||||||
|  | 
 | ||||||
|  | 	// Foo is an example field of OnePasswordItem. Edit onepassworditem_types.go to remove/update | ||||||
| 	ItemPath string `json:"itemPath,omitempty"` | 	ItemPath string `json:"itemPath,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // OnePasswordItemStatus defines the observed state of OnePasswordItem | // OnePasswordItemStatus defines the observed state of OnePasswordItem | ||||||
| type OnePasswordItemStatus struct { | type OnePasswordItemStatus struct { | ||||||
| 	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster | 	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster | ||||||
| 	// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file | 	// Important: Run "make" to regenerate code after modifying this file | ||||||
| 	// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | //+kubebuilder:object:root=true | ||||||
|  | //+kubebuilder:subresource:status | ||||||
| 
 | 
 | ||||||
| // OnePasswordItem is the Schema for the onepassworditems API | // OnePasswordItem is the Schema for the onepassworditems API | ||||||
| // +kubebuilder:subresource:status |  | ||||||
| // +kubebuilder:resource:path=onepassworditems,scope=Namespaced |  | ||||||
| type OnePasswordItem struct { | type OnePasswordItem struct { | ||||||
| 	metav1.TypeMeta   `json:",inline"` | 	metav1.TypeMeta   `json:",inline"` | ||||||
| 	metav1.ObjectMeta `json:"metadata,omitempty"` | 	metav1.ObjectMeta `json:"metadata,omitempty"` | ||||||
|  | 	Type              string `json:"type,omitempty"` | ||||||
| 
 | 
 | ||||||
| 	Spec   OnePasswordItemSpec   `json:"spec,omitempty"` | 	Spec   OnePasswordItemSpec   `json:"spec,omitempty"` | ||||||
| 	Status OnePasswordItemStatus `json:"status,omitempty"` | 	Status OnePasswordItemStatus `json:"status,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | //+kubebuilder:object:root=true | ||||||
| 
 | 
 | ||||||
| // OnePasswordItemList contains a list of OnePasswordItem | // OnePasswordItemList contains a list of OnePasswordItem | ||||||
| type OnePasswordItemList struct { | type OnePasswordItemList struct { | ||||||
| @@ -1,6 +1,23 @@ | |||||||
|  | //go:build !ignore_autogenerated | ||||||
| // +build !ignore_autogenerated | // +build !ignore_autogenerated | ||||||
| 
 | 
 | ||||||
| // Code generated by operator-sdk. DO NOT EDIT. | /* | ||||||
|  | Copyright 2022. | ||||||
|  | 
 | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  | 
 | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | 
 | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | // Code generated by controller-gen. DO NOT EDIT. | ||||||
| 
 | 
 | ||||||
| package v1 | package v1 | ||||||
| 
 | 
 | ||||||
| @@ -15,7 +32,6 @@ func (in *OnePasswordItem) DeepCopyInto(out *OnePasswordItem) { | |||||||
| 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) | 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) | ||||||
| 	out.Spec = in.Spec | 	out.Spec = in.Spec | ||||||
| 	out.Status = in.Status | 	out.Status = in.Status | ||||||
| 	return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem. | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem. | ||||||
| @@ -48,7 +64,6 @@ func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) { | |||||||
| 			(*in)[i].DeepCopyInto(&(*out)[i]) | 			(*in)[i].DeepCopyInto(&(*out)[i]) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemList. | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemList. | ||||||
| @@ -72,7 +87,6 @@ func (in *OnePasswordItemList) DeepCopyObject() runtime.Object { | |||||||
| // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. | ||||||
| func (in *OnePasswordItemSpec) DeepCopyInto(out *OnePasswordItemSpec) { | func (in *OnePasswordItemSpec) DeepCopyInto(out *OnePasswordItemSpec) { | ||||||
| 	*out = *in | 	*out = *in | ||||||
| 	return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemSpec. | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemSpec. | ||||||
| @@ -88,7 +102,6 @@ func (in *OnePasswordItemSpec) DeepCopy() *OnePasswordItemSpec { | |||||||
| // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. | ||||||
| func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) { | func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) { | ||||||
| 	*out = *in | 	*out = *in | ||||||
| 	return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus. | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus. | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| FROM registry.access.redhat.com/ubi8/ubi-minimal:latest |  | ||||||
|  |  | ||||||
| ENV OPERATOR=/usr/local/bin/onepassword-connect-operator \ |  | ||||||
|     USER_UID=1001 \ |  | ||||||
|     USER_NAME=onepassword-connect-operator |  | ||||||
|  |  | ||||||
| # install operator binary |  | ||||||
| COPY build/_output/bin/op-kubernetes-connect-operator ${OPERATOR} |  | ||||||
|  |  | ||||||
| COPY build/bin /usr/local/bin |  | ||||||
| RUN  /usr/local/bin/user_setup |  | ||||||
|  |  | ||||||
| ENTRYPOINT ["/usr/local/bin/entrypoint"] |  | ||||||
|  |  | ||||||
| USER ${USER_UID} |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| #!/bin/sh -e |  | ||||||
|  |  | ||||||
| exec ${OPERATOR} $@ |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| #!/bin/sh |  | ||||||
| set -x |  | ||||||
|  |  | ||||||
| # ensure $HOME exists and is accessible by group 0 (we don't know what the runtime UID will be) |  | ||||||
| echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd |  | ||||||
| mkdir -p "${HOME}" |  | ||||||
| chown "${USER_UID}:0" "${HOME}" |  | ||||||
| chmod ug+rwx "${HOME}" |  | ||||||
|  |  | ||||||
| # no need for this script to remain in the image after running |  | ||||||
| rm "$0" |  | ||||||
| @@ -1,251 +0,0 @@ | |||||||
| package main |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"flag" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"runtime" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/controller" |  | ||||||
| 	op "github.com/1Password/onepassword-operator/pkg/onepassword" |  | ||||||
|  |  | ||||||
| 	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) |  | ||||||
|  |  | ||||||
| 	_ "k8s.io/client-go/plugin/pkg/client/auth" |  | ||||||
| 	"k8s.io/client-go/rest" |  | ||||||
|  |  | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/apis" |  | ||||||
| 	"github.com/1Password/onepassword-operator/version" |  | ||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/connect" |  | ||||||
|  |  | ||||||
| 	"github.com/operator-framework/operator-sdk/pkg/k8sutil" |  | ||||||
| 	kubemetrics "github.com/operator-framework/operator-sdk/pkg/kube-metrics" |  | ||||||
| 	"github.com/operator-framework/operator-sdk/pkg/leader" |  | ||||||
| 	"github.com/operator-framework/operator-sdk/pkg/log/zap" |  | ||||||
| 	"github.com/operator-framework/operator-sdk/pkg/metrics" |  | ||||||
| 	sdkVersion "github.com/operator-framework/operator-sdk/version" |  | ||||||
| 	"github.com/spf13/pflag" |  | ||||||
| 	v1 "k8s.io/api/core/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/util/intstr" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/cache" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/config" |  | ||||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/manager" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/manager/signals" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const envPollingIntervalVariable = "POLLING_INTERVAL" |  | ||||||
| const defaultPollingInterval = 600 |  | ||||||
|  |  | ||||||
| // Change below variables to serve metrics on different host or port. |  | ||||||
| var ( |  | ||||||
| 	metricsHost               = "0.0.0.0" |  | ||||||
| 	metricsPort         int32 = 8383 |  | ||||||
| 	operatorMetricsPort int32 = 8686 |  | ||||||
| ) |  | ||||||
| var log = logf.Log.WithName("cmd") |  | ||||||
|  |  | ||||||
| func printVersion() { |  | ||||||
| 	log.Info(fmt.Sprintf("Operator Version: %s", version.Version)) |  | ||||||
| 	log.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) |  | ||||||
| 	log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) |  | ||||||
| 	log.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func main() { |  | ||||||
| 	// Add the zap logger flag set to the CLI. The flag set must |  | ||||||
| 	// be added before calling pflag.Parse(). |  | ||||||
| 	pflag.CommandLine.AddFlagSet(zap.FlagSet()) |  | ||||||
|  |  | ||||||
| 	// Add flags registered by imported packages (e.g. glog and |  | ||||||
| 	// controller-runtime) |  | ||||||
| 	pflag.CommandLine.AddGoFlagSet(flag.CommandLine) |  | ||||||
|  |  | ||||||
| 	pflag.Parse() |  | ||||||
|  |  | ||||||
| 	// Use a zap logr.Logger implementation. If none of the zap |  | ||||||
| 	// flags are configured (or if the zap flag set is not being |  | ||||||
| 	// used), this defaults to a production zap logger. |  | ||||||
| 	// |  | ||||||
| 	// The logger instantiated here can be changed to any logger |  | ||||||
| 	// implementing the logr.Logger interface. This logger will |  | ||||||
| 	// be propagated through the whole operator, generating |  | ||||||
| 	// uniform and structured logs. |  | ||||||
| 	logf.SetLogger(zap.Logger()) |  | ||||||
|  |  | ||||||
| 	printVersion() |  | ||||||
|  |  | ||||||
| 	namespace, err := k8sutil.GetWatchNamespace() |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error(err, "Failed to get watch namespace") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Get a config to talk to the apiserver |  | ||||||
| 	cfg, err := config.GetConfig() |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error(err, "") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	// Become the leader before proceeding |  | ||||||
| 	err = leader.Become(ctx, "onepassword-connect-operator-lock") |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error(err, "") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Set default manager options |  | ||||||
| 	options := manager.Options{ |  | ||||||
| 		Namespace:          namespace, |  | ||||||
| 		MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2) |  | ||||||
| 	// Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate |  | ||||||
| 	// Also note that you may face performance issues when using this with a high number of namespaces. |  | ||||||
| 	if strings.Contains(namespace, ",") { |  | ||||||
| 		options.Namespace = "" |  | ||||||
| 		options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(namespace, ",")) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Create a new manager to provide shared dependencies and start components |  | ||||||
| 	mgr, err := manager.New(cfg, options) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error(err, "") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Info("Registering Components.") |  | ||||||
|  |  | ||||||
| 	// Setup Scheme for all resources |  | ||||||
| 	if err := apis.AddToScheme(mgr.GetScheme()); err != nil { |  | ||||||
| 		log.Error(err, "") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Setup One Password Client |  | ||||||
| 	opConnectClient, err := connect.NewClientFromEnvironment() |  | ||||||
|  |  | ||||||
| 	if err := controller.AddToManager(mgr, opConnectClient); err != nil { |  | ||||||
| 		log.Error(err, "") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Add the Metrics Service |  | ||||||
| 	addMetrics(ctx, cfg) |  | ||||||
|  |  | ||||||
| 	// Setup update secrets task |  | ||||||
| 	updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient) |  | ||||||
| 	done := make(chan bool) |  | ||||||
| 	ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets()) |  | ||||||
| 	go func() { |  | ||||||
| 		for { |  | ||||||
| 			select { |  | ||||||
| 			case <-done: |  | ||||||
| 				ticker.Stop() |  | ||||||
| 				return |  | ||||||
| 			case <-ticker.C: |  | ||||||
| 				updatedSecretsPoller.UpdateKubernetesSecretsTask() |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	// Start the Cmd |  | ||||||
| 	if err := mgr.Start(signals.SetupSignalHandler()); err != nil { |  | ||||||
| 		log.Error(err, "Manager exited non-zero") |  | ||||||
| 		done <- true |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // addMetrics will create the Services and Service Monitors to allow the operator export the metrics by using |  | ||||||
| // the Prometheus operator |  | ||||||
| func addMetrics(ctx context.Context, cfg *rest.Config) { |  | ||||||
| 	// Get the namespace the operator is currently deployed in. |  | ||||||
| 	operatorNs, err := k8sutil.GetOperatorNamespace() |  | ||||||
| 	if err != nil { |  | ||||||
| 		if errors.Is(err, k8sutil.ErrRunLocal) { |  | ||||||
| 			log.Info("Skipping CR metrics server creation; not running in a cluster.") |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := serveCRMetrics(cfg, operatorNs); err != nil { |  | ||||||
| 		log.Info("Could not generate and serve custom resource metrics", "error", err.Error()) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Add to the below struct any other metrics ports you want to expose. |  | ||||||
| 	servicePorts := []v1.ServicePort{ |  | ||||||
| 		{Port: metricsPort, Name: metrics.OperatorPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: metricsPort}}, |  | ||||||
| 		{Port: operatorMetricsPort, Name: metrics.CRPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: operatorMetricsPort}}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Create Service object to expose the metrics port(s). |  | ||||||
| 	service, err := metrics.CreateMetricsService(ctx, cfg, servicePorts) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Info("Could not create metrics Service", "error", err.Error()) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// CreateServiceMonitors will automatically create the prometheus-operator ServiceMonitor resources |  | ||||||
| 	// necessary to configure Prometheus to scrape metrics from this operator. |  | ||||||
| 	services := []*v1.Service{service} |  | ||||||
|  |  | ||||||
| 	// The ServiceMonitor is created in the same namespace where the operator is deployed |  | ||||||
| 	_, err = metrics.CreateServiceMonitors(cfg, operatorNs, services) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Info("Could not create ServiceMonitor object", "error", err.Error()) |  | ||||||
| 		// If this operator is deployed to a cluster without the prometheus-operator running, it will return |  | ||||||
| 		// ErrServiceMonitorNotPresent, which can be used to safely skip ServiceMonitor creation. |  | ||||||
| 		if err == metrics.ErrServiceMonitorNotPresent { |  | ||||||
| 			log.Info("Install prometheus-operator in your cluster to create ServiceMonitor objects", "error", err.Error()) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // serveCRMetrics gets the Operator/CustomResource GVKs and generates metrics based on those types. |  | ||||||
| // It serves those metrics on "http://metricsHost:operatorMetricsPort". |  | ||||||
| func serveCRMetrics(cfg *rest.Config, operatorNs string) error { |  | ||||||
| 	// The function below returns a list of filtered operator/CR specific GVKs. For more control, override the GVK list below |  | ||||||
| 	// with your own custom logic. Note that if you are adding third party API schemas, probably you will need to |  | ||||||
| 	// customize this implementation to avoid permissions issues. |  | ||||||
| 	filteredGVK, err := k8sutil.GetGVKsFromAddToScheme(apis.AddToScheme) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// The metrics will be generated from the namespaces which are returned here. |  | ||||||
| 	// NOTE that passing nil or an empty list of namespaces in GenerateAndServeCRMetrics will result in an error. |  | ||||||
| 	ns, err := kubemetrics.GetNamespacesForMetrics(operatorNs) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Generate and serve custom resource specific metrics. |  | ||||||
| 	err = kubemetrics.GenerateAndServeCRMetrics(cfg, ns, filteredGVK, metricsHost, operatorMetricsPort) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func getPollingIntervalForUpdatingSecrets() time.Duration { |  | ||||||
| 	timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable) |  | ||||||
| 	if found { |  | ||||||
| 		timeInSeconds, err := strconv.Atoi(timeInSecondsString) |  | ||||||
| 		if err == nil { |  | ||||||
| 			return time.Duration(timeInSeconds) * time.Second |  | ||||||
| 		} |  | ||||||
| 		log.Info("Invalid value set for polling interval. Must be a valid integer.") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval)) |  | ||||||
| 	return time.Duration(defaultPollingInterval) * time.Second |  | ||||||
| } |  | ||||||
							
								
								
									
										45
									
								
								config/crd/bases/onepassword.com_onepassworditems_crd.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								config/crd/bases/onepassword.com_onepassworditems_crd.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | apiVersion: apiextensions.k8s.io/v1 | ||||||
|  | kind: CustomResourceDefinition | ||||||
|  | metadata: | ||||||
|  |   name: onepassworditems.onepassword.com | ||||||
|  | spec: | ||||||
|  |   group: onepassword.com | ||||||
|  |   names: | ||||||
|  |     kind: OnePasswordItem | ||||||
|  |     listKind: OnePasswordItemList | ||||||
|  |     plural: onepassworditems | ||||||
|  |     singular: onepassworditem | ||||||
|  |   scope: Namespaced | ||||||
|  |   versions: | ||||||
|  |   - name: v1 | ||||||
|  |     served: true | ||||||
|  |     storage: true | ||||||
|  |     schema: | ||||||
|  |       openAPIV3Schema: | ||||||
|  |         description: OnePasswordItem is the Schema for the onepassworditems API | ||||||
|  |         properties: | ||||||
|  |           apiVersion: | ||||||
|  |             description: 'APIVersion defines the versioned schema of this representation | ||||||
|  |               of an object. Servers should convert recognized schemas to the latest | ||||||
|  |               internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' | ||||||
|  |             type: string | ||||||
|  |           kind: | ||||||
|  |             description: 'Kind is a string value representing the REST resource this | ||||||
|  |               object represents. Servers may infer this from the endpoint the client | ||||||
|  |               submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' | ||||||
|  |             type: string | ||||||
|  |           metadata: | ||||||
|  |             type: object | ||||||
|  |           spec: | ||||||
|  |             description: OnePasswordItemSpec defines the desired state of OnePasswordItem | ||||||
|  |             properties: | ||||||
|  |               itemPath: | ||||||
|  |                 type: string | ||||||
|  |             type: object | ||||||
|  |           status: | ||||||
|  |             description: OnePasswordItemStatus defines the observed state of OnePasswordItem | ||||||
|  |             type: object | ||||||
|  |           type: | ||||||
|  |             description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types' | ||||||
|  |             type: string | ||||||
|  |         type: object | ||||||
							
								
								
									
										68
									
								
								config/crd/connect/deployment.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								config/crd/connect/deployment.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | |||||||
|  | apiVersion: apps/v1 | ||||||
|  | kind: Deployment | ||||||
|  | metadata: | ||||||
|  |   name: onepassword-connect | ||||||
|  | spec: | ||||||
|  |   selector: | ||||||
|  |     matchLabels: | ||||||
|  |       app: onepassword-connect | ||||||
|  |   template: | ||||||
|  |     metadata: | ||||||
|  |       labels: | ||||||
|  |         app: onepassword-connect | ||||||
|  |         version: "1.0.0" | ||||||
|  |     spec: | ||||||
|  |       volumes: | ||||||
|  |         - name: shared-data | ||||||
|  |           emptyDir: {} | ||||||
|  |         - name: credentials | ||||||
|  |           secret: | ||||||
|  |             secretName: op-credentials | ||||||
|  |       initContainers: | ||||||
|  |         - name: sqlite-permissions | ||||||
|  |           image: alpine:3.12 | ||||||
|  |           command: | ||||||
|  |             - "/bin/sh" | ||||||
|  |             - "-c" | ||||||
|  |           args: | ||||||
|  |             - "mkdir -p /home/opuser/.op/data && chown -R 999 /home/opuser && chmod -R 700 /home/opuser && chmod -f -R 600 /home/opuser/.op/config || :" | ||||||
|  |           volumeMounts: | ||||||
|  |             - mountPath: /home/opuser/.op/data | ||||||
|  |               name: shared-data | ||||||
|  |       containers: | ||||||
|  |         - name: connect-api | ||||||
|  |           image: 1password/connect-api:latest | ||||||
|  |           resources: | ||||||
|  |             limits: | ||||||
|  |               memory: "128Mi" | ||||||
|  |               cpu: "0.2" | ||||||
|  |           ports: | ||||||
|  |             - containerPort: 8080 | ||||||
|  |           env: | ||||||
|  |             - name: OP_SESSION | ||||||
|  |               valueFrom: | ||||||
|  |                 secretKeyRef: | ||||||
|  |                   name: op-credentials | ||||||
|  |                   key: op-session | ||||||
|  |           volumeMounts: | ||||||
|  |             - mountPath: /home/opuser/.op/data | ||||||
|  |               name: shared-data | ||||||
|  |         - name: connect-sync | ||||||
|  |           image: 1password/connect-sync:latest | ||||||
|  |           resources: | ||||||
|  |             limits: | ||||||
|  |               memory: "128Mi" | ||||||
|  |               cpu: "0.2" | ||||||
|  |           ports: | ||||||
|  |             - containerPort: 8081 | ||||||
|  |           env: | ||||||
|  |             - name: OP_HTTP_PORT | ||||||
|  |               value: "8081" | ||||||
|  |             - name: OP_SESSION | ||||||
|  |               valueFrom: | ||||||
|  |                 secretKeyRef: | ||||||
|  |                   name: op-credentials | ||||||
|  |                   key: op-session | ||||||
|  |           volumeMounts: | ||||||
|  |             - mountPath: /home/opuser/.op/data | ||||||
|  |               name: shared-data | ||||||
							
								
								
									
										15
									
								
								config/crd/connect/service.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								config/crd/connect/service.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | apiVersion: v1 | ||||||
|  | kind: Service | ||||||
|  | metadata: | ||||||
|  |   name: onepassword-connect | ||||||
|  | spec: | ||||||
|  |   type: NodePort | ||||||
|  |   selector: | ||||||
|  |     app: onepassword-connect | ||||||
|  |   ports: | ||||||
|  |     - port: 8080 | ||||||
|  |       name: connect-api | ||||||
|  |       nodePort: 31080 | ||||||
|  |     - port: 8081 | ||||||
|  |       name: connect-sync | ||||||
|  |       nodePort: 31081 | ||||||
							
								
								
									
										21
									
								
								config/crd/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								config/crd/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | # This kustomization.yaml is not intended to be run by itself, | ||||||
|  | # since it depends on service name and namespace that are out of this kustomize package. | ||||||
|  | # It should be run by config/default | ||||||
|  | resources: | ||||||
|  | - bases/onepassword.onepassword.com_onepassworditems.yaml | ||||||
|  | #+kubebuilder:scaffold:crdkustomizeresource | ||||||
|  |  | ||||||
|  | patchesStrategicMerge: | ||||||
|  | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. | ||||||
|  | # patches here are for enabling the conversion webhook for each CRD | ||||||
|  | #- patches/webhook_in_onepassworditems.yaml | ||||||
|  | #+kubebuilder:scaffold:crdkustomizewebhookpatch | ||||||
|  |  | ||||||
|  | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. | ||||||
|  | # patches here are for enabling the CA injection for each CRD | ||||||
|  | #- patches/cainjection_in_onepassworditems.yaml | ||||||
|  | #+kubebuilder:scaffold:crdkustomizecainjectionpatch | ||||||
|  |  | ||||||
|  | # the following config is for teaching kustomize how to do kustomization for CRDs. | ||||||
|  | configurations: | ||||||
|  | - kustomizeconfig.yaml | ||||||
							
								
								
									
										19
									
								
								config/crd/kustomizeconfig.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								config/crd/kustomizeconfig.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | # This file is for teaching kustomize how to substitute name and namespace reference in CRD | ||||||
|  | nameReference: | ||||||
|  | - kind: Service | ||||||
|  |   version: v1 | ||||||
|  |   fieldSpecs: | ||||||
|  |   - kind: CustomResourceDefinition | ||||||
|  |     version: v1 | ||||||
|  |     group: apiextensions.k8s.io | ||||||
|  |     path: spec/conversion/webhook/clientConfig/service/name | ||||||
|  |  | ||||||
|  | namespace: | ||||||
|  | - kind: CustomResourceDefinition | ||||||
|  |   version: v1 | ||||||
|  |   group: apiextensions.k8s.io | ||||||
|  |   path: spec/conversion/webhook/clientConfig/service/namespace | ||||||
|  |   create: false | ||||||
|  |  | ||||||
|  | varReference: | ||||||
|  | - path: metadata/annotations | ||||||
| @@ -17,7 +17,6 @@ spec: | |||||||
|         - name: onepassword-connect-operator |         - name: onepassword-connect-operator | ||||||
|           image: 1password/onepassword-operator |           image: 1password/onepassword-operator | ||||||
|           command: ["/manager"] |           command: ["/manager"] | ||||||
|           imagePullPolicy: Never |  | ||||||
|           env: |           env: | ||||||
|             - name: WATCH_NAMESPACE |             - name: WATCH_NAMESPACE | ||||||
|               value: "default" |               value: "default" | ||||||
| @@ -28,7 +27,7 @@ spec: | |||||||
|             - name: OPERATOR_NAME |             - name: OPERATOR_NAME | ||||||
|               value: "onepassword-connect-operator" |               value: "onepassword-connect-operator" | ||||||
|             - name: OP_CONNECT_HOST |             - name: OP_CONNECT_HOST | ||||||
|               value: "http://secret-service:8080" |               value: "http://onepassword-connect:8080" | ||||||
|             - name: POLLING_INTERVAL |             - name: POLLING_INTERVAL | ||||||
|               value: "10" |               value: "10" | ||||||
|             - name: OP_CONNECT_TOKEN |             - name: OP_CONNECT_TOKEN | ||||||
| @@ -36,3 +35,5 @@ spec: | |||||||
|                 secretKeyRef: |                 secretKeyRef: | ||||||
|                   name: onepassword-token |                   name: onepassword-token | ||||||
|                   key: token |                   key: token | ||||||
|  |             - name: AUTO_RESTART | ||||||
|  |               value: "false" | ||||||
| @@ -16,9 +16,7 @@ spec: | |||||||
|       containers: |       containers: | ||||||
|         - name: onepassword-connect-operator |         - name: onepassword-connect-operator | ||||||
|           image: 1password/onepassword-operator |           image: 1password/onepassword-operator | ||||||
|           command: |           command: ["/manager"] | ||||||
|           - onepassword-connect-operator |  | ||||||
|           imagePullPolicy: Never |  | ||||||
|           env: |           env: | ||||||
|             - name: WATCH_NAMESPACE |             - name: WATCH_NAMESPACE | ||||||
|               value: "default,development" |               value: "default,development" | ||||||
| @@ -29,7 +27,7 @@ spec: | |||||||
|             - name: OPERATOR_NAME |             - name: OPERATOR_NAME | ||||||
|               value: "onepassword-connect-operator" |               value: "onepassword-connect-operator" | ||||||
|             - name: OP_CONNECT_HOST |             - name: OP_CONNECT_HOST | ||||||
|               value: "http://secret-service:8080" |               value: "http://onepassword-connect:8080" | ||||||
|             - name: POLLING_INTERVAL |             - name: POLLING_INTERVAL | ||||||
|               value: "10" |               value: "10" | ||||||
|             - name: OP_CONNECT_TOKEN |             - name: OP_CONNECT_TOKEN | ||||||
| @@ -37,3 +35,5 @@ spec: | |||||||
|                 secretKeyRef: |                 secretKeyRef: | ||||||
|                   name: onepassword-token |                   name: onepassword-token | ||||||
|                   key: token |                   key: token | ||||||
|  |             - name: AUTO_RESTART | ||||||
|  |               value: "false" | ||||||
							
								
								
									
										7
									
								
								config/crd/patches/cainjection_in_onepassworditems.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								config/crd/patches/cainjection_in_onepassworditems.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | # The following patch adds a directive for certmanager to inject CA into the CRD | ||||||
|  | apiVersion: apiextensions.k8s.io/v1 | ||||||
|  | kind: CustomResourceDefinition | ||||||
|  | metadata: | ||||||
|  |   annotations: | ||||||
|  |     cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) | ||||||
|  |   name: onepassworditems.onepassword.onepassword.com | ||||||
							
								
								
									
										16
									
								
								config/crd/patches/webhook_in_onepassworditems.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								config/crd/patches/webhook_in_onepassworditems.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | # The following patch enables a conversion webhook for the CRD | ||||||
|  | apiVersion: apiextensions.k8s.io/v1 | ||||||
|  | kind: CustomResourceDefinition | ||||||
|  | metadata: | ||||||
|  |   name: onepassworditems.onepassword.onepassword.com | ||||||
|  | spec: | ||||||
|  |   conversion: | ||||||
|  |     strategy: Webhook | ||||||
|  |     webhook: | ||||||
|  |       clientConfig: | ||||||
|  |         service: | ||||||
|  |           namespace: system | ||||||
|  |           name: webhook-service | ||||||
|  |           path: /convert | ||||||
|  |       conversionReviewVersions: | ||||||
|  |       - v1 | ||||||
| @@ -3,7 +3,7 @@ kind: ServiceAccount | |||||||
| metadata: | metadata: | ||||||
|   name: onepassword-connect-operator |   name: onepassword-connect-operator | ||||||
| --- | --- | ||||||
| kind: RoleBinding | kind: ClusterRoleBinding | ||||||
| apiVersion: rbac.authorization.k8s.io/v1 | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
| metadata: | metadata: | ||||||
|   name: onepassword-connect-operator-default |   name: onepassword-connect-operator-default | ||||||
| @@ -13,12 +13,12 @@ subjects: | |||||||
|   name: onepassword-connect-operator |   name: onepassword-connect-operator | ||||||
|   namespace: default |   namespace: default | ||||||
| roleRef: | roleRef: | ||||||
|   kind: Role |   kind: ClusterRole | ||||||
|   name: onepassword-connect-operator |   name: onepassword-connect-operator | ||||||
|   apiGroup: rbac.authorization.k8s.io |   apiGroup: rbac.authorization.k8s.io | ||||||
| --- | --- | ||||||
| apiVersion: rbac.authorization.k8s.io/v1 | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
| kind: Role | kind: ClusterRole | ||||||
| metadata: | metadata: | ||||||
|   creationTimestamp: null |   creationTimestamp: null | ||||||
|   name: onepassword-connect-operator |   name: onepassword-connect-operator | ||||||
| @@ -34,6 +34,7 @@ rules: | |||||||
|   - events |   - events | ||||||
|   - configmaps |   - configmaps | ||||||
|   - secrets |   - secrets | ||||||
|  |   - namespaces | ||||||
|   verbs: |   verbs: | ||||||
|   - create |   - create | ||||||
|   - delete |   - delete | ||||||
| @@ -3,7 +3,7 @@ kind: ServiceAccount | |||||||
| metadata: | metadata: | ||||||
|   name: onepassword-connect-operator |   name: onepassword-connect-operator | ||||||
| --- | --- | ||||||
| kind: RoleBinding | kind: ClusterRoleBinding | ||||||
| apiVersion: rbac.authorization.k8s.io/v1 | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
| metadata: | metadata: | ||||||
|   name: onepassword-connect-operator-default |   name: onepassword-connect-operator-default | ||||||
| @@ -17,7 +17,7 @@ roleRef: | |||||||
|   name: onepassword-connect-operator |   name: onepassword-connect-operator | ||||||
|   apiGroup: rbac.authorization.k8s.io |   apiGroup: rbac.authorization.k8s.io | ||||||
| --- | --- | ||||||
| kind: RoleBinding | kind: ClusterRoleBinding | ||||||
| apiVersion: rbac.authorization.k8s.io/v1 | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
| metadata: | metadata: | ||||||
|   name: onepassword-connect-operator-development |   name: onepassword-connect-operator-development | ||||||
| @@ -48,6 +48,7 @@ rules: | |||||||
|   - events |   - events | ||||||
|   - configmaps |   - configmaps | ||||||
|   - secrets |   - secrets | ||||||
|  |   - namespaces | ||||||
|   verbs: |   verbs: | ||||||
|   - create |   - create | ||||||
|   - delete |   - delete | ||||||
							
								
								
									
										74
									
								
								config/default/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								config/default/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | # Adds namespace to all resources. | ||||||
|  | namespace: onepassword-operator-new-system | ||||||
|  |  | ||||||
|  | # Value of this field is prepended to the | ||||||
|  | # names of all resources, e.g. a deployment named | ||||||
|  | # "wordpress" becomes "alices-wordpress". | ||||||
|  | # Note that it should also match with the prefix (text before '-') of the namespace | ||||||
|  | # field above. | ||||||
|  | namePrefix: onepassword-operator-new- | ||||||
|  |  | ||||||
|  | # Labels to add to all resources and selectors. | ||||||
|  | #commonLabels: | ||||||
|  | #  someName: someValue | ||||||
|  |  | ||||||
|  | bases: | ||||||
|  | - ../crd | ||||||
|  | - ../rbac | ||||||
|  | - ../manager | ||||||
|  | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in | ||||||
|  | # crd/kustomization.yaml | ||||||
|  | #- ../webhook | ||||||
|  | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. | ||||||
|  | #- ../certmanager | ||||||
|  | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. | ||||||
|  | #- ../prometheus | ||||||
|  |  | ||||||
|  | patchesStrategicMerge: | ||||||
|  | # Protect the /metrics endpoint by putting it behind auth. | ||||||
|  | # If you want your controller-manager to expose the /metrics | ||||||
|  | # endpoint w/o any authn/z, please comment the following line. | ||||||
|  | - manager_auth_proxy_patch.yaml | ||||||
|  |  | ||||||
|  | # Mount the controller config file for loading manager configurations | ||||||
|  | # through a ComponentConfig type | ||||||
|  | #- manager_config_patch.yaml | ||||||
|  |  | ||||||
|  | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in | ||||||
|  | # crd/kustomization.yaml | ||||||
|  | #- manager_webhook_patch.yaml | ||||||
|  |  | ||||||
|  | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. | ||||||
|  | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. | ||||||
|  | # 'CERTMANAGER' needs to be enabled to use ca injection | ||||||
|  | #- webhookcainjection_patch.yaml | ||||||
|  |  | ||||||
|  | # the following config is for teaching kustomize how to do var substitution | ||||||
|  | vars: | ||||||
|  | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. | ||||||
|  | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR | ||||||
|  | #  objref: | ||||||
|  | #    kind: Certificate | ||||||
|  | #    group: cert-manager.io | ||||||
|  | #    version: v1 | ||||||
|  | #    name: serving-cert # this name should match the one in certificate.yaml | ||||||
|  | #  fieldref: | ||||||
|  | #    fieldpath: metadata.namespace | ||||||
|  | #- name: CERTIFICATE_NAME | ||||||
|  | #  objref: | ||||||
|  | #    kind: Certificate | ||||||
|  | #    group: cert-manager.io | ||||||
|  | #    version: v1 | ||||||
|  | #    name: serving-cert # this name should match the one in certificate.yaml | ||||||
|  | #- name: SERVICE_NAMESPACE # namespace of the service | ||||||
|  | #  objref: | ||||||
|  | #    kind: Service | ||||||
|  | #    version: v1 | ||||||
|  | #    name: webhook-service | ||||||
|  | #  fieldref: | ||||||
|  | #    fieldpath: metadata.namespace | ||||||
|  | #- name: SERVICE_NAME | ||||||
|  | #  objref: | ||||||
|  | #    kind: Service | ||||||
|  | #    version: v1 | ||||||
|  | #    name: webhook-service | ||||||
							
								
								
									
										34
									
								
								config/default/manager_auth_proxy_patch.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								config/default/manager_auth_proxy_patch.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | # This patch inject a sidecar container which is a HTTP proxy for the | ||||||
|  | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. | ||||||
|  | apiVersion: apps/v1 | ||||||
|  | kind: Deployment | ||||||
|  | metadata: | ||||||
|  |   name: controller-manager | ||||||
|  |   namespace: system | ||||||
|  | spec: | ||||||
|  |   template: | ||||||
|  |     spec: | ||||||
|  |       containers: | ||||||
|  |       - name: kube-rbac-proxy | ||||||
|  |         image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 | ||||||
|  |         args: | ||||||
|  |         - "--secure-listen-address=0.0.0.0:8443" | ||||||
|  |         - "--upstream=http://127.0.0.1:8080/" | ||||||
|  |         - "--logtostderr=true" | ||||||
|  |         - "--v=0" | ||||||
|  |         ports: | ||||||
|  |         - containerPort: 8443 | ||||||
|  |           protocol: TCP | ||||||
|  |           name: https | ||||||
|  |         resources: | ||||||
|  |           limits: | ||||||
|  |             cpu: 500m | ||||||
|  |             memory: 128Mi | ||||||
|  |           requests: | ||||||
|  |             cpu: 5m | ||||||
|  |             memory: 64Mi | ||||||
|  |       - name: manager | ||||||
|  |         args: | ||||||
|  |         - "--health-probe-bind-address=:8081" | ||||||
|  |         - "--metrics-bind-address=127.0.0.1:8080" | ||||||
|  |         - "--leader-elect" | ||||||
							
								
								
									
										20
									
								
								config/default/manager_config_patch.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								config/default/manager_config_patch.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | apiVersion: apps/v1 | ||||||
|  | kind: Deployment | ||||||
|  | metadata: | ||||||
|  |   name: controller-manager | ||||||
|  |   namespace: system | ||||||
|  | spec: | ||||||
|  |   template: | ||||||
|  |     spec: | ||||||
|  |       containers: | ||||||
|  |       - name: manager | ||||||
|  |         args: | ||||||
|  |         - "--config=controller_manager_config.yaml" | ||||||
|  |         volumeMounts: | ||||||
|  |         - name: manager-config | ||||||
|  |           mountPath: /controller_manager_config.yaml | ||||||
|  |           subPath: controller_manager_config.yaml | ||||||
|  |       volumes: | ||||||
|  |       - name: manager-config | ||||||
|  |         configMap: | ||||||
|  |           name: manager-config | ||||||
							
								
								
									
										11
									
								
								config/manager/controller_manager_config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								config/manager/controller_manager_config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 | ||||||
|  | kind: ControllerManagerConfig | ||||||
|  | health: | ||||||
|  |   healthProbeBindAddress: :8081 | ||||||
|  | metrics: | ||||||
|  |   bindAddress: 127.0.0.1:8080 | ||||||
|  | webhook: | ||||||
|  |   port: 9443 | ||||||
|  | leaderElection: | ||||||
|  |   leaderElect: true | ||||||
|  |   resourceName: c26807fd.onepassword.com | ||||||
							
								
								
									
										10
									
								
								config/manager/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								config/manager/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | resources: | ||||||
|  | - manager.yaml | ||||||
|  |  | ||||||
|  | generatorOptions: | ||||||
|  |   disableNameSuffixHash: true | ||||||
|  |  | ||||||
|  | configMapGenerator: | ||||||
|  | - name: manager-config | ||||||
|  |   files: | ||||||
|  |   - controller_manager_config.yaml | ||||||
							
								
								
									
										60
									
								
								config/manager/manager.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								config/manager/manager.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | apiVersion: v1 | ||||||
|  | kind: Namespace | ||||||
|  | metadata: | ||||||
|  |   labels: | ||||||
|  |     control-plane: controller-manager | ||||||
|  |   name: system | ||||||
|  | --- | ||||||
|  | apiVersion: apps/v1 | ||||||
|  | kind: Deployment | ||||||
|  | metadata: | ||||||
|  |   name: controller-manager | ||||||
|  |   namespace: system | ||||||
|  |   labels: | ||||||
|  |     control-plane: controller-manager | ||||||
|  | spec: | ||||||
|  |   selector: | ||||||
|  |     matchLabels: | ||||||
|  |       control-plane: controller-manager | ||||||
|  |   replicas: 1 | ||||||
|  |   template: | ||||||
|  |     metadata: | ||||||
|  |       annotations: | ||||||
|  |         kubectl.kubernetes.io/default-container: manager | ||||||
|  |       labels: | ||||||
|  |         control-plane: controller-manager | ||||||
|  |     spec: | ||||||
|  |       securityContext: | ||||||
|  |         runAsNonRoot: true | ||||||
|  |       containers: | ||||||
|  |       - command: | ||||||
|  |         - /manager | ||||||
|  |         args: | ||||||
|  |         - --leader-elect | ||||||
|  |         image: controller:latest | ||||||
|  |         name: manager | ||||||
|  |         securityContext: | ||||||
|  |           allowPrivilegeEscalation: false | ||||||
|  |         livenessProbe: | ||||||
|  |           httpGet: | ||||||
|  |             path: /healthz | ||||||
|  |             port: 8081 | ||||||
|  |           initialDelaySeconds: 15 | ||||||
|  |           periodSeconds: 20 | ||||||
|  |         readinessProbe: | ||||||
|  |           httpGet: | ||||||
|  |             path: /readyz | ||||||
|  |             port: 8081 | ||||||
|  |           initialDelaySeconds: 5 | ||||||
|  |           periodSeconds: 10 | ||||||
|  |         # TODO(user): Configure the resources accordingly based on the project requirements. | ||||||
|  |         # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ | ||||||
|  |         resources: | ||||||
|  |           limits: | ||||||
|  |             cpu: 500m | ||||||
|  |             memory: 128Mi | ||||||
|  |           requests: | ||||||
|  |             cpu: 10m | ||||||
|  |             memory: 64Mi | ||||||
|  |       serviceAccountName: controller-manager | ||||||
|  |       terminationGracePeriodSeconds: 10 | ||||||
							
								
								
									
										27
									
								
								config/manifests/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								config/manifests/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | # These resources constitute the fully configured set of manifests | ||||||
|  | # used to generate the 'manifests/' directory in a bundle. | ||||||
|  | resources: | ||||||
|  | - bases/onepassword-operator-new.clusterserviceversion.yaml | ||||||
|  | - ../default | ||||||
|  | - ../samples | ||||||
|  | - ../scorecard | ||||||
|  |  | ||||||
|  | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. | ||||||
|  | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. | ||||||
|  | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. | ||||||
|  | #patchesJson6902: | ||||||
|  | #- target: | ||||||
|  | #    group: apps | ||||||
|  | #    version: v1 | ||||||
|  | #    kind: Deployment | ||||||
|  | #    name: controller-manager | ||||||
|  | #    namespace: system | ||||||
|  | #  patch: |- | ||||||
|  | #    # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. | ||||||
|  | #    # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. | ||||||
|  | #    - op: remove | ||||||
|  | #      path: /spec/template/spec/containers/1/volumeMounts/0 | ||||||
|  | #    # Remove the "cert" volume, since OLM will create and mount a set of certs. | ||||||
|  | #    # Update the indices in this path if adding or removing volumes in the manager's Deployment. | ||||||
|  | #    - op: remove | ||||||
|  | #      path: /spec/template/spec/volumes/0 | ||||||
							
								
								
									
										2
									
								
								config/prometheus/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								config/prometheus/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | resources: | ||||||
|  | - monitor.yaml | ||||||
							
								
								
									
										20
									
								
								config/prometheus/monitor.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								config/prometheus/monitor.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  |  | ||||||
|  | # Prometheus Monitor Service (Metrics) | ||||||
|  | apiVersion: monitoring.coreos.com/v1 | ||||||
|  | kind: ServiceMonitor | ||||||
|  | metadata: | ||||||
|  |   labels: | ||||||
|  |     control-plane: controller-manager | ||||||
|  |   name: controller-manager-metrics-monitor | ||||||
|  |   namespace: system | ||||||
|  | spec: | ||||||
|  |   endpoints: | ||||||
|  |     - path: /metrics | ||||||
|  |       port: https | ||||||
|  |       scheme: https | ||||||
|  |       bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token | ||||||
|  |       tlsConfig: | ||||||
|  |         insecureSkipVerify: true | ||||||
|  |   selector: | ||||||
|  |     matchLabels: | ||||||
|  |       control-plane: controller-manager | ||||||
							
								
								
									
										9
									
								
								config/rbac/auth_proxy_client_clusterrole.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								config/rbac/auth_proxy_client_clusterrole.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRole | ||||||
|  | metadata: | ||||||
|  |   name: metrics-reader | ||||||
|  | rules: | ||||||
|  | - nonResourceURLs: | ||||||
|  |   - "/metrics" | ||||||
|  |   verbs: | ||||||
|  |   - get | ||||||
							
								
								
									
										17
									
								
								config/rbac/auth_proxy_role.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								config/rbac/auth_proxy_role.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRole | ||||||
|  | metadata: | ||||||
|  |   name: proxy-role | ||||||
|  | rules: | ||||||
|  | - apiGroups: | ||||||
|  |   - authentication.k8s.io | ||||||
|  |   resources: | ||||||
|  |   - tokenreviews | ||||||
|  |   verbs: | ||||||
|  |   - create | ||||||
|  | - apiGroups: | ||||||
|  |   - authorization.k8s.io | ||||||
|  |   resources: | ||||||
|  |   - subjectaccessreviews | ||||||
|  |   verbs: | ||||||
|  |   - create | ||||||
							
								
								
									
										12
									
								
								config/rbac/auth_proxy_role_binding.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								config/rbac/auth_proxy_role_binding.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRoleBinding | ||||||
|  | metadata: | ||||||
|  |   name: proxy-rolebinding | ||||||
|  | roleRef: | ||||||
|  |   apiGroup: rbac.authorization.k8s.io | ||||||
|  |   kind: ClusterRole | ||||||
|  |   name: proxy-role | ||||||
|  | subjects: | ||||||
|  | - kind: ServiceAccount | ||||||
|  |   name: controller-manager | ||||||
|  |   namespace: system | ||||||
							
								
								
									
										15
									
								
								config/rbac/auth_proxy_service.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								config/rbac/auth_proxy_service.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | apiVersion: v1 | ||||||
|  | kind: Service | ||||||
|  | metadata: | ||||||
|  |   labels: | ||||||
|  |     control-plane: controller-manager | ||||||
|  |   name: controller-manager-metrics-service | ||||||
|  |   namespace: system | ||||||
|  | spec: | ||||||
|  |   ports: | ||||||
|  |   - name: https | ||||||
|  |     port: 8443 | ||||||
|  |     protocol: TCP | ||||||
|  |     targetPort: https | ||||||
|  |   selector: | ||||||
|  |     control-plane: controller-manager | ||||||
							
								
								
									
										18
									
								
								config/rbac/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								config/rbac/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | resources: | ||||||
|  | # All RBAC will be applied under this service account in | ||||||
|  | # the deployment namespace. You may comment out this resource | ||||||
|  | # if your manager will use a service account that exists at | ||||||
|  | # runtime. Be sure to update RoleBinding and ClusterRoleBinding | ||||||
|  | # subjects if changing service account names. | ||||||
|  | - service_account.yaml | ||||||
|  | - role.yaml | ||||||
|  | - role_binding.yaml | ||||||
|  | - leader_election_role.yaml | ||||||
|  | - leader_election_role_binding.yaml | ||||||
|  | # Comment the following 4 lines if you want to disable | ||||||
|  | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) | ||||||
|  | # which protects your /metrics endpoint. | ||||||
|  | - auth_proxy_service.yaml | ||||||
|  | - auth_proxy_role.yaml | ||||||
|  | - auth_proxy_role_binding.yaml | ||||||
|  | - auth_proxy_client_clusterrole.yaml | ||||||
							
								
								
									
										37
									
								
								config/rbac/leader_election_role.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								config/rbac/leader_election_role.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | # permissions to do leader election. | ||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: Role | ||||||
|  | metadata: | ||||||
|  |   name: leader-election-role | ||||||
|  | rules: | ||||||
|  | - apiGroups: | ||||||
|  |   - "" | ||||||
|  |   resources: | ||||||
|  |   - configmaps | ||||||
|  |   verbs: | ||||||
|  |   - get | ||||||
|  |   - list | ||||||
|  |   - watch | ||||||
|  |   - create | ||||||
|  |   - update | ||||||
|  |   - patch | ||||||
|  |   - delete | ||||||
|  | - apiGroups: | ||||||
|  |   - coordination.k8s.io | ||||||
|  |   resources: | ||||||
|  |   - leases | ||||||
|  |   verbs: | ||||||
|  |   - get | ||||||
|  |   - list | ||||||
|  |   - watch | ||||||
|  |   - create | ||||||
|  |   - update | ||||||
|  |   - patch | ||||||
|  |   - delete | ||||||
|  | - apiGroups: | ||||||
|  |   - "" | ||||||
|  |   resources: | ||||||
|  |   - events | ||||||
|  |   verbs: | ||||||
|  |   - create | ||||||
|  |   - patch | ||||||
							
								
								
									
										12
									
								
								config/rbac/leader_election_role_binding.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								config/rbac/leader_election_role_binding.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: RoleBinding | ||||||
|  | metadata: | ||||||
|  |   name: leader-election-rolebinding | ||||||
|  | roleRef: | ||||||
|  |   apiGroup: rbac.authorization.k8s.io | ||||||
|  |   kind: Role | ||||||
|  |   name: leader-election-role | ||||||
|  | subjects: | ||||||
|  | - kind: ServiceAccount | ||||||
|  |   name: controller-manager | ||||||
|  |   namespace: system | ||||||
							
								
								
									
										24
									
								
								config/rbac/onepassworditem_editor_role.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								config/rbac/onepassworditem_editor_role.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | # permissions for end users to edit onepassworditems. | ||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRole | ||||||
|  | metadata: | ||||||
|  |   name: onepassworditem-editor-role | ||||||
|  | rules: | ||||||
|  | - apiGroups: | ||||||
|  |   - onepassword.onepassword.com | ||||||
|  |   resources: | ||||||
|  |   - onepassworditems | ||||||
|  |   verbs: | ||||||
|  |   - create | ||||||
|  |   - delete | ||||||
|  |   - get | ||||||
|  |   - list | ||||||
|  |   - patch | ||||||
|  |   - update | ||||||
|  |   - watch | ||||||
|  | - apiGroups: | ||||||
|  |   - onepassword.onepassword.com | ||||||
|  |   resources: | ||||||
|  |   - onepassworditems/status | ||||||
|  |   verbs: | ||||||
|  |   - get | ||||||
							
								
								
									
										20
									
								
								config/rbac/onepassworditem_viewer_role.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								config/rbac/onepassworditem_viewer_role.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | # permissions for end users to view onepassworditems. | ||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRole | ||||||
|  | metadata: | ||||||
|  |   name: onepassworditem-viewer-role | ||||||
|  | rules: | ||||||
|  | - apiGroups: | ||||||
|  |   - onepassword.onepassword.com | ||||||
|  |   resources: | ||||||
|  |   - onepassworditems | ||||||
|  |   verbs: | ||||||
|  |   - get | ||||||
|  |   - list | ||||||
|  |   - watch | ||||||
|  | - apiGroups: | ||||||
|  |   - onepassword.onepassword.com | ||||||
|  |   resources: | ||||||
|  |   - onepassworditems/status | ||||||
|  |   verbs: | ||||||
|  |   - get | ||||||
							
								
								
									
										12
									
								
								config/rbac/role_binding.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								config/rbac/role_binding.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRoleBinding | ||||||
|  | metadata: | ||||||
|  |   name: manager-rolebinding | ||||||
|  | roleRef: | ||||||
|  |   apiGroup: rbac.authorization.k8s.io | ||||||
|  |   kind: ClusterRole | ||||||
|  |   name: manager-role | ||||||
|  | subjects: | ||||||
|  | - kind: ServiceAccount | ||||||
|  |   name: controller-manager | ||||||
|  |   namespace: system | ||||||
							
								
								
									
										5
									
								
								config/rbac/service_account.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								config/rbac/service_account.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | apiVersion: v1 | ||||||
|  | kind: ServiceAccount | ||||||
|  | metadata: | ||||||
|  |   name: controller-manager | ||||||
|  |   namespace: system | ||||||
							
								
								
									
										4
									
								
								config/samples/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								config/samples/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | ## Append samples you want in your CSV to this file as resources ## | ||||||
|  | resources: | ||||||
|  | - onepassword_v1_onepassworditem.yaml | ||||||
|  | #+kubebuilder:scaffold:manifestskustomizesamples | ||||||
							
								
								
									
										6
									
								
								config/samples/onepassword_v1_onepassworditem.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								config/samples/onepassword_v1_onepassworditem.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | apiVersion: onepassword.onepassword.com/v1 | ||||||
|  | kind: OnePasswordItem | ||||||
|  | metadata: | ||||||
|  |   name: onepassworditem-sample | ||||||
|  | spec: | ||||||
|  |   # TODO(user): Add fields here | ||||||
							
								
								
									
										7
									
								
								config/scorecard/bases/config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								config/scorecard/bases/config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | apiVersion: scorecard.operatorframework.io/v1alpha3 | ||||||
|  | kind: Configuration | ||||||
|  | metadata: | ||||||
|  |   name: config | ||||||
|  | stages: | ||||||
|  | - parallel: true | ||||||
|  |   tests: [] | ||||||
							
								
								
									
										16
									
								
								config/scorecard/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								config/scorecard/kustomization.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | resources: | ||||||
|  | - bases/config.yaml | ||||||
|  | patchesJson6902: | ||||||
|  | - path: patches/basic.config.yaml | ||||||
|  |   target: | ||||||
|  |     group: scorecard.operatorframework.io | ||||||
|  |     version: v1alpha3 | ||||||
|  |     kind: Configuration | ||||||
|  |     name: config | ||||||
|  | - path: patches/olm.config.yaml | ||||||
|  |   target: | ||||||
|  |     group: scorecard.operatorframework.io | ||||||
|  |     version: v1alpha3 | ||||||
|  |     kind: Configuration | ||||||
|  |     name: config | ||||||
|  | #+kubebuilder:scaffold:patchesJson6902 | ||||||
							
								
								
									
										10
									
								
								config/scorecard/patches/basic.config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								config/scorecard/patches/basic.config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | - op: add | ||||||
|  |   path: /stages/0/tests/- | ||||||
|  |   value: | ||||||
|  |     entrypoint: | ||||||
|  |     - scorecard-test | ||||||
|  |     - basic-check-spec | ||||||
|  |     image: quay.io/operator-framework/scorecard-test:v1.19.0 | ||||||
|  |     labels: | ||||||
|  |       suite: basic | ||||||
|  |       test: basic-check-spec-test | ||||||
							
								
								
									
										50
									
								
								config/scorecard/patches/olm.config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								config/scorecard/patches/olm.config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | - op: add | ||||||
|  |   path: /stages/0/tests/- | ||||||
|  |   value: | ||||||
|  |     entrypoint: | ||||||
|  |     - scorecard-test | ||||||
|  |     - olm-bundle-validation | ||||||
|  |     image: quay.io/operator-framework/scorecard-test:v1.19.0 | ||||||
|  |     labels: | ||||||
|  |       suite: olm | ||||||
|  |       test: olm-bundle-validation-test | ||||||
|  | - op: add | ||||||
|  |   path: /stages/0/tests/- | ||||||
|  |   value: | ||||||
|  |     entrypoint: | ||||||
|  |     - scorecard-test | ||||||
|  |     - olm-crds-have-validation | ||||||
|  |     image: quay.io/operator-framework/scorecard-test:v1.19.0 | ||||||
|  |     labels: | ||||||
|  |       suite: olm | ||||||
|  |       test: olm-crds-have-validation-test | ||||||
|  | - op: add | ||||||
|  |   path: /stages/0/tests/- | ||||||
|  |   value: | ||||||
|  |     entrypoint: | ||||||
|  |     - scorecard-test | ||||||
|  |     - olm-crds-have-resources | ||||||
|  |     image: quay.io/operator-framework/scorecard-test:v1.19.0 | ||||||
|  |     labels: | ||||||
|  |       suite: olm | ||||||
|  |       test: olm-crds-have-resources-test | ||||||
|  | - op: add | ||||||
|  |   path: /stages/0/tests/- | ||||||
|  |   value: | ||||||
|  |     entrypoint: | ||||||
|  |     - scorecard-test | ||||||
|  |     - olm-spec-descriptors | ||||||
|  |     image: quay.io/operator-framework/scorecard-test:v1.19.0 | ||||||
|  |     labels: | ||||||
|  |       suite: olm | ||||||
|  |       test: olm-spec-descriptors-test | ||||||
|  | - op: add | ||||||
|  |   path: /stages/0/tests/- | ||||||
|  |   value: | ||||||
|  |     entrypoint: | ||||||
|  |     - scorecard-test | ||||||
|  |     - olm-status-descriptors | ||||||
|  |     image: quay.io/operator-framework/scorecard-test:v1.19.0 | ||||||
|  |     labels: | ||||||
|  |       suite: olm | ||||||
|  |       test: olm-status-descriptors-test | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package deployment | package controllers | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| @@ -14,9 +14,11 @@ import ( | |||||||
| 	appsv1 "k8s.io/api/apps/v1" | 	appsv1 "k8s.io/api/apps/v1" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/api/errors" | 	"k8s.io/apimachinery/pkg/api/errors" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
| 	ctrl "sigs.k8s.io/controller-runtime" | 	ctrl "sigs.k8s.io/controller-runtime" | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil" | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/controller" | 	"sigs.k8s.io/controller-runtime/pkg/controller" | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/handler" | 	"sigs.k8s.io/controller-runtime/pkg/handler" | ||||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" | 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
| @@ -25,10 +27,10 @@ import ( | |||||||
| 	"sigs.k8s.io/controller-runtime/pkg/source" | 	"sigs.k8s.io/controller-runtime/pkg/source" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var log = logf.Log.WithName("controller_deployment") | var deploymentLog = logf.Log.WithName("controller_deployment") | ||||||
| var finalizer = "onepassword.com/finalizer.secret" | var finalizer = "onepassword.com/finalizer.secret" | ||||||
| 
 | 
 | ||||||
| const annotationRegExpString = "^onepasswordoperator\\/[a-zA-Z\\.]+" | const annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" | ||||||
| 
 | 
 | ||||||
| func Add(mgr manager.Manager, opConnectClient connect.Client) error { | func Add(mgr manager.Manager, opConnectClient connect.Client) error { | ||||||
| 	return add(mgr, newReconciler(mgr, opConnectClient)) | 	return add(mgr, newReconciler(mgr, opConnectClient)) | ||||||
| @@ -69,9 +71,23 @@ type ReconcileDeployment struct { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *ReconcileDeployment) SetupWithManager(mgr ctrl.Manager) error { | func (r *ReconcileDeployment) SetupWithManager(mgr ctrl.Manager) error { | ||||||
| 	return ctrl.NewControllerManagedBy(mgr). | 
 | ||||||
| 		For(&appsv1.Deployment{}). | 	c, err := controller.New("deployment-controller", mgr, controller.Options{Reconciler: r}) | ||||||
| 		Complete(r) | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Watch for changes to primary resource Deployment | ||||||
|  | 	err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | 	// TODO figure out what to do with this code. | ||||||
|  | 	// return ctrl.NewControllerManagedBy(mgr). | ||||||
|  | 	// 	For(&appsv1.Deployment{}). | ||||||
|  | 	// 	Complete(r) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *ReconcileDeployment) test() { | func (r *ReconcileDeployment) test() { | ||||||
| @@ -83,9 +99,8 @@ func (r *ReconcileDeployment) test() { | |||||||
| // Note: | // Note: | ||||||
| // The Controller will requeue the Request to be processed again if the returned error is non-nil or | // The Controller will requeue the Request to be processed again if the returned error is non-nil or | ||||||
| // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. | // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. | ||||||
| func (r *ReconcileDeployment) Reconcile(request reconcile.Request) (reconcile.Result, error) { | func (r *ReconcileDeployment) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { | ||||||
| 	ctx := context.Background() | 	reqLogger := deploymentLog.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) | ||||||
| 	reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) |  | ||||||
| 	reqLogger.Info("Reconciling Deployment") | 	reqLogger.Info("Reconciling Deployment") | ||||||
| 
 | 
 | ||||||
| 	deployment := &appsv1.Deployment{} | 	deployment := &appsv1.Deployment{} | ||||||
| @@ -99,7 +114,7 @@ func (r *ReconcileDeployment) Reconcile(request reconcile.Request) (reconcile.Re | |||||||
| 
 | 
 | ||||||
| 	annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.opAnnotationRegExp) | 	annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.opAnnotationRegExp) | ||||||
| 	if !annotationsFound { | 	if !annotationsFound { | ||||||
| 		reqLogger.Info("No One Password Annotations found") | 		reqLogger.Info("No 1Password Annotations found") | ||||||
| 		return reconcile.Result{}, nil | 		return reconcile.Result{}, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -114,7 +129,7 @@ func (r *ReconcileDeployment) Reconcile(request reconcile.Request) (reconcile.Re | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		// Handles creation or updating secrets for deployment if needed | 		// Handles creation or updating secrets for deployment if needed | ||||||
| 		if err := r.HandleApplyingDeployment(deployment.Namespace, annotations, request); err != nil { | 		if err := r.HandleApplyingDeployment(deployment, deployment.Namespace, annotations, request); err != nil { | ||||||
| 			return reconcile.Result{}, err | 			return reconcile.Result{}, err | ||||||
| 		} | 		} | ||||||
| 		return reconcile.Result{}, nil | 		return reconcile.Result{}, nil | ||||||
| @@ -142,7 +157,7 @@ func (r *ReconcileDeployment) cleanupKubernetesSecretForDeployment(secretName st | |||||||
| 	if len(secretName) == 0 { | 	if len(secretName) == 0 { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	updatedSecrets := map[string]bool{secretName: true} | 	updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret} | ||||||
| 
 | 
 | ||||||
| 	multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment) | 	multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -160,7 +175,7 @@ func (r *ReconcileDeployment) cleanupKubernetesSecretForDeployment(secretName st | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *ReconcileDeployment) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]bool, deletedDeployment appsv1.Deployment) (bool, error) { | func (r *ReconcileDeployment) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) { | ||||||
| 	deployments := &appsv1.DeploymentList{} | 	deployments := &appsv1.DeploymentList{} | ||||||
| 	opts := []client.ListOption{ | 	opts := []client.ListOption{ | ||||||
| 		client.InNamespace(deletedDeployment.Namespace), | 		client.InNamespace(deletedDeployment.Namespace), | ||||||
| @@ -168,7 +183,7 @@ func (r *ReconcileDeployment) areMultipleDeploymentsUsingSecret(updatedSecrets m | |||||||
| 
 | 
 | ||||||
| 	err := r.kubeClient.List(context.Background(), deployments, opts...) | 	err := r.kubeClient.List(context.Background(), deployments, opts...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error(err, "Failed to list kubernetes deployments") | 		deploymentLog.Error(err, "Failed to list kubernetes deployments") | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -187,10 +202,13 @@ func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deploymen | |||||||
| 	return r.kubeClient.Update(context.Background(), deployment) | 	return r.kubeClient.Update(context.Background(), deployment) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotations map[string]string, request reconcile.Request) error { | func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error { | ||||||
| 	reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) | 	reqLog := deploymentLog.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) | ||||||
| 
 | 
 | ||||||
| 	secretName := annotations[op.NameAnnotation] | 	secretName := annotations[op.NameAnnotation] | ||||||
|  | 	secretLabels := map[string]string(nil) | ||||||
|  | 	secretType := "" | ||||||
|  | 
 | ||||||
| 	if len(secretName) == 0 { | 	if len(secretName) == 0 { | ||||||
| 		reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.") | 		reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.") | ||||||
| 		return nil | 		return nil | ||||||
| @@ -201,5 +219,17 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat | |||||||
| 		return fmt.Errorf("Failed to retrieve item: %v", err) | 		return fmt.Errorf("Failed to retrieve item: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item) | 	// Create owner reference. | ||||||
|  | 	gvk, err := apiutil.GVKForObject(deployment, r.scheme) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("could not to retrieve group version kind: %v", err) | ||||||
|  | 	} | ||||||
|  | 	ownerRef := &metav1.OwnerReference{ | ||||||
|  | 		APIVersion: gvk.GroupVersion().String(), | ||||||
|  | 		Kind:       gvk.Kind, | ||||||
|  | 		Name:       deployment.GetName(), | ||||||
|  | 		UID:        deployment.GetUID(), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, annotations, ownerRef) | ||||||
| } | } | ||||||
							
								
								
									
										183
									
								
								controllers/onepassworditem_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								controllers/onepassworditem_controller.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,183 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package controllers | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"github.com/1Password/onepassword-operator/pkg/onepassword" | ||||||
|  | 	op "github.com/1Password/onepassword-operator/pkg/onepassword" | ||||||
|  |  | ||||||
|  | 	kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/errors" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	ctrl "sigs.k8s.io/controller-runtime" | ||||||
|  | 	kubeClient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/controller" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/handler" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/source" | ||||||
|  |  | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil" | ||||||
|  | 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/reconcile" | ||||||
|  |  | ||||||
|  | 	"github.com/1Password/onepassword-operator/pkg/utils" | ||||||
|  |  | ||||||
|  | 	"github.com/1Password/connect-sdk-go/connect" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  |  | ||||||
|  | 	onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var log = logf.Log.WithName("controller_onepassworditem") | ||||||
|  |  | ||||||
|  | // OnePasswordItemReconciler reconciles a OnePasswordItem object | ||||||
|  | type OnePasswordItemReconciler struct { | ||||||
|  | 	Client          kubeClient.Client | ||||||
|  | 	Scheme          *runtime.Scheme | ||||||
|  | 	OpConnectClient connect.Client | ||||||
|  | } | ||||||
|  |  | ||||||
|  | //+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete | ||||||
|  | //+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems/status,verbs=get;update;patch | ||||||
|  | //+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems/finalizers,verbs=update | ||||||
|  |  | ||||||
|  | // Reconcile is part of the main kubernetes reconciliation loop which aims to | ||||||
|  | // move the current state of the cluster closer to the desired state. | ||||||
|  | // TODO(user): Modify the Reconcile function to compare the state specified by | ||||||
|  | // the OnePasswordItem object against the actual cluster state, and then | ||||||
|  | // perform operations to make the cluster state reflect the state specified by | ||||||
|  | // the user. | ||||||
|  | // | ||||||
|  | // For more details, check Reconcile and its Result here: | ||||||
|  | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile | ||||||
|  | func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { | ||||||
|  | 	reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) | ||||||
|  | 	reqLogger.Info("Reconciling OnePasswordItem") | ||||||
|  |  | ||||||
|  | 	onepassworditem := &onepasswordv1.OnePasswordItem{} | ||||||
|  | 	err := r.Client.Get(context.Background(), request.NamespacedName, onepassworditem) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if errors.IsNotFound(err) { | ||||||
|  | 			return reconcile.Result{}, nil | ||||||
|  | 		} | ||||||
|  | 		return reconcile.Result{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If the deployment is not being deleted | ||||||
|  | 	if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
|  | 		// Adds a finalizer to the deployment if one does not exist. | ||||||
|  | 		// This is so we can handle cleanup of associated secrets properly | ||||||
|  | 		if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { | ||||||
|  | 			onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer) | ||||||
|  | 			if err := r.Client.Update(context.Background(), onepassworditem); err != nil { | ||||||
|  | 				return reconcile.Result{}, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Handles creation or updating secrets for deployment if needed | ||||||
|  | 		if err := r.HandleOnePasswordItem(onepassworditem, request); err != nil { | ||||||
|  | 			return reconcile.Result{}, err | ||||||
|  | 		} | ||||||
|  | 		return reconcile.Result{}, nil | ||||||
|  | 	} | ||||||
|  | 	// If one password finalizer exists then we must cleanup associated secrets | ||||||
|  | 	if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { | ||||||
|  |  | ||||||
|  | 		// Delete associated kubernetes secret | ||||||
|  | 		if err = r.cleanupKubernetesSecret(onepassworditem); err != nil { | ||||||
|  | 			return reconcile.Result{}, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Remove finalizer now that cleanup is complete | ||||||
|  | 		if err := r.removeFinalizer(onepassworditem); err != nil { | ||||||
|  | 			return reconcile.Result{}, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return reconcile.Result{}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetupWithManager sets up the controller with the Manager. | ||||||
|  | func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error { | ||||||
|  | 	c, err := controller.New("onepassworditem-controller", mgr, controller.Options{Reconciler: r}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Watch for changes to primary resource OnePasswordItem | ||||||
|  | 	err = c.Watch(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | 	// TODO Consider the simplified code below. Based on the migration guide: https://sdk.operatorframework.io/docs/building-operators/golang/migration/#create-a-new-project | ||||||
|  | 	//	return ctrl.NewControllerManagedBy(mgr).Named("onepassworditem-controller").WithOptions(controller.Options{Reconciler: r}). | ||||||
|  | 	//		For(&onepasswordv1.OnePasswordItem{}).Watches(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{}). | ||||||
|  | 	//		Complete(r) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *OnePasswordItemReconciler) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error { | ||||||
|  | 	onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer) | ||||||
|  | 	if err := r.Client.Update(context.Background(), onePasswordItem); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error { | ||||||
|  | 	kubernetesSecret := &corev1.Secret{} | ||||||
|  | 	kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name | ||||||
|  | 	kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace | ||||||
|  |  | ||||||
|  | 	r.Client.Delete(context.Background(), kubernetesSecret) | ||||||
|  | 	if err := r.Client.Delete(context.Background(), kubernetesSecret); err != nil { | ||||||
|  | 		if !errors.IsNotFound(err) { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *OnePasswordItemReconciler) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error { | ||||||
|  | 	secretName := resource.GetName() | ||||||
|  | 	labels := resource.Labels | ||||||
|  | 	annotations := resource.Annotations | ||||||
|  | 	secretType := resource.Type | ||||||
|  | 	autoRestart := annotations[op.RestartDeploymentsAnnotation] | ||||||
|  |  | ||||||
|  | 	item, err := onepassword.GetOnePasswordItemByPath(r.OpConnectClient, resource.Spec.ItemPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("Failed to retrieve item: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Create owner reference. | ||||||
|  | 	gvk, err := apiutil.GVKForObject(resource, r.Scheme) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("could not to retrieve group version kind: %v", err) | ||||||
|  | 	} | ||||||
|  | 	ownerRef := &metav1.OwnerReference{ | ||||||
|  | 		APIVersion: gvk.GroupVersion().String(), | ||||||
|  | 		Kind:       gvk.Kind, | ||||||
|  | 		Name:       resource.GetName(), | ||||||
|  | 		UID:        resource.GetUID(), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return kubeSecrets.CreateKubernetesSecretFromItem(r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, annotations, ownerRef) | ||||||
|  | } | ||||||
							
								
								
									
										80
									
								
								controllers/suite_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								controllers/suite_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package controllers | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	. "github.com/onsi/ginkgo" | ||||||
|  | 	. "github.com/onsi/gomega" | ||||||
|  | 	"k8s.io/client-go/kubernetes/scheme" | ||||||
|  | 	"k8s.io/client-go/rest" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/envtest" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/envtest/printer" | ||||||
|  | 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/log/zap" | ||||||
|  |  | ||||||
|  | 	onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" | ||||||
|  | 	//+kubebuilder:scaffold:imports | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // These tests use Ginkgo (BDD-style Go testing framework). Refer to | ||||||
|  | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. | ||||||
|  |  | ||||||
|  | var cfg *rest.Config | ||||||
|  | var k8sClient client.Client | ||||||
|  | var testEnv *envtest.Environment | ||||||
|  |  | ||||||
|  | func TestAPIs(t *testing.T) { | ||||||
|  | 	RegisterFailHandler(Fail) | ||||||
|  |  | ||||||
|  | 	RunSpecsWithDefaultAndCustomReporters(t, | ||||||
|  | 		"Controller Suite", | ||||||
|  | 		[]Reporter{printer.NewlineReporter{}}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ = BeforeSuite(func() { | ||||||
|  | 	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) | ||||||
|  |  | ||||||
|  | 	By("bootstrapping test environment") | ||||||
|  | 	testEnv = &envtest.Environment{ | ||||||
|  | 		CRDDirectoryPaths:     []string{filepath.Join("..", "config", "crd", "bases")}, | ||||||
|  | 		ErrorIfCRDPathMissing: true, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cfg, err := testEnv.Start() | ||||||
|  | 	Expect(err).NotTo(HaveOccurred()) | ||||||
|  | 	Expect(cfg).NotTo(BeNil()) | ||||||
|  |  | ||||||
|  | 	err = onepasswordv1.AddToScheme(scheme.Scheme) | ||||||
|  | 	Expect(err).NotTo(HaveOccurred()) | ||||||
|  |  | ||||||
|  | 	//+kubebuilder:scaffold:scheme | ||||||
|  |  | ||||||
|  | 	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) | ||||||
|  | 	Expect(err).NotTo(HaveOccurred()) | ||||||
|  | 	Expect(k8sClient).NotTo(BeNil()) | ||||||
|  |  | ||||||
|  | }, 60) | ||||||
|  |  | ||||||
|  | var _ = AfterSuite(func() { | ||||||
|  | 	By("tearing down the test environment") | ||||||
|  | 	err := testEnv.Stop() | ||||||
|  | 	Expect(err).NotTo(HaveOccurred()) | ||||||
|  | }) | ||||||
| @@ -1,45 +0,0 @@ | |||||||
| apiVersion: apiextensions.k8s.io/v1beta1 |  | ||||||
| kind: CustomResourceDefinition |  | ||||||
| metadata: |  | ||||||
|   name: onepassworditems.onepassword.com |  | ||||||
| spec: |  | ||||||
|   group: onepassword.com |  | ||||||
|   names: |  | ||||||
|     kind: OnePasswordItem |  | ||||||
|     listKind: OnePasswordItemList |  | ||||||
|     plural: onepassworditems |  | ||||||
|     singular: onepassworditem |  | ||||||
|   scope: Namespaced |  | ||||||
|   subresources: |  | ||||||
|     status: {} |  | ||||||
|   validation: |  | ||||||
|     openAPIV3Schema: |  | ||||||
|       description: OnePasswordItem is the Schema for the onepassworditems API |  | ||||||
|       properties: |  | ||||||
|         apiVersion: |  | ||||||
|           description: 'APIVersion defines the versioned schema of this representation |  | ||||||
|             of an object. Servers should convert recognized schemas to the latest |  | ||||||
|             internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' |  | ||||||
|           type: string |  | ||||||
|         kind: |  | ||||||
|           description: 'Kind is a string value representing the REST resource this |  | ||||||
|             object represents. Servers may infer this from the endpoint the client |  | ||||||
|             submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' |  | ||||||
|           type: string |  | ||||||
|         metadata: |  | ||||||
|           type: object |  | ||||||
|         spec: |  | ||||||
|           description: OnePasswordItemSpec defines the desired state of OnePasswordItem |  | ||||||
|           properties: |  | ||||||
|             item_path: |  | ||||||
|               type: string |  | ||||||
|           type: object |  | ||||||
|         status: |  | ||||||
|           description: OnePasswordItemStatus defines the observed state of OnePasswordItem |  | ||||||
|           type: object |  | ||||||
|       type: object |  | ||||||
|   version: v1 |  | ||||||
|   versions: |  | ||||||
|   - name: v1 |  | ||||||
|     served: true |  | ||||||
|     storage: true |  | ||||||
							
								
								
									
										92
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,25 +1,81 @@ | |||||||
| module github.com/1Password/onepassword-operator | module github.com/1Password/onepassword-operator | ||||||
|  |  | ||||||
| go 1.13 | go 1.17 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/1Password/connect-sdk-go v0.0.1 | 	github.com/1Password/connect-sdk-go v1.2.0 | ||||||
| 	github.com/go-logr/logr v0.1.0 // indirect | 	github.com/onsi/ginkgo v1.16.5 | ||||||
| 	github.com/operator-framework/operator-sdk v0.19.0 | 	github.com/onsi/gomega v1.17.0 | ||||||
| 	github.com/pkg/errors v0.9.1 // indirect | 	github.com/stretchr/testify v1.7.0 | ||||||
| 	github.com/prometheus/common v0.14.0 // indirect | 	k8s.io/api v0.23.5 | ||||||
| 	github.com/sirupsen/logrus v1.7.0 // indirect | 	k8s.io/apimachinery v0.23.5 | ||||||
| 	github.com/spf13/pflag v1.0.5 | 	k8s.io/client-go v0.23.5 | ||||||
| 	github.com/stretchr/testify v1.6.1 | 	k8s.io/kubectl v0.23.5 | ||||||
| 	go.etcd.io/etcd v3.3.25+incompatible // indirect | 	sigs.k8s.io/controller-runtime v0.11.0 | ||||||
| 	k8s.io/api v0.18.2 |  | ||||||
| 	k8s.io/apimachinery v0.18.2 |  | ||||||
| 	k8s.io/client-go v12.0.0+incompatible |  | ||||||
| 	k8s.io/kubectl v0.18.2 |  | ||||||
| 	sigs.k8s.io/controller-runtime v0.6.0 |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| replace ( | require ( | ||||||
| 	github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM | 	cloud.google.com/go v0.81.0 // indirect | ||||||
| 	k8s.io/client-go => k8s.io/client-go v0.18.2 // Required by prometheus-operator | 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect | ||||||
|  | 	github.com/Azure/go-autorest/autorest v0.11.18 // indirect | ||||||
|  | 	github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect | ||||||
|  | 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect | ||||||
|  | 	github.com/Azure/go-autorest/logger v0.2.1 // indirect | ||||||
|  | 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect | ||||||
|  | 	github.com/beorn7/perks v1.0.1 // indirect | ||||||
|  | 	github.com/cespare/xxhash/v2 v2.1.1 // indirect | ||||||
|  | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
|  | 	github.com/evanphx/json-patch v4.12.0+incompatible // indirect | ||||||
|  | 	github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect | ||||||
|  | 	github.com/fsnotify/fsnotify v1.5.1 // indirect | ||||||
|  | 	github.com/go-logr/logr v1.2.0 // indirect | ||||||
|  | 	github.com/go-logr/zapr v1.2.0 // indirect | ||||||
|  | 	github.com/gogo/protobuf v1.3.2 // indirect | ||||||
|  | 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | ||||||
|  | 	github.com/golang/protobuf v1.5.2 // indirect | ||||||
|  | 	github.com/google/go-cmp v0.5.5 // indirect | ||||||
|  | 	github.com/google/gofuzz v1.1.0 // indirect | ||||||
|  | 	github.com/google/uuid v1.1.2 // indirect | ||||||
|  | 	github.com/googleapis/gnostic v0.5.5 // indirect | ||||||
|  | 	github.com/imdario/mergo v0.3.12 // indirect | ||||||
|  | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
|  | 	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect | ||||||
|  | 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||||
|  | 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||||
|  | 	github.com/nxadm/tail v1.4.8 // indirect | ||||||
|  | 	github.com/opentracing/opentracing-go v1.2.0 // indirect | ||||||
|  | 	github.com/pkg/errors v0.9.1 // indirect | ||||||
|  | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
|  | 	github.com/prometheus/client_golang v1.11.0 // indirect | ||||||
|  | 	github.com/prometheus/client_model v0.2.0 // indirect | ||||||
|  | 	github.com/prometheus/common v0.28.0 // indirect | ||||||
|  | 	github.com/prometheus/procfs v0.6.0 // indirect | ||||||
|  | 	github.com/spf13/pflag v1.0.5 // indirect | ||||||
|  | 	github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect | ||||||
|  | 	github.com/uber/jaeger-lib v2.4.0+incompatible // indirect | ||||||
|  | 	go.uber.org/atomic v1.7.0 // indirect | ||||||
|  | 	go.uber.org/multierr v1.6.0 // indirect | ||||||
|  | 	go.uber.org/zap v1.19.1 // indirect | ||||||
|  | 	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect | ||||||
|  | 	golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect | ||||||
|  | 	golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect | ||||||
|  | 	golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect | ||||||
|  | 	golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect | ||||||
|  | 	golang.org/x/text v0.3.7 // indirect | ||||||
|  | 	golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect | ||||||
|  | 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect | ||||||
|  | 	google.golang.org/appengine v1.6.7 // indirect | ||||||
|  | 	google.golang.org/protobuf v1.27.1 // indirect | ||||||
|  | 	gopkg.in/inf.v0 v0.9.1 // indirect | ||||||
|  | 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect | ||||||
|  | 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||||
|  | 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect | ||||||
|  | 	k8s.io/apiextensions-apiserver v0.23.0 // indirect | ||||||
|  | 	k8s.io/component-base v0.23.5 // indirect | ||||||
|  | 	k8s.io/klog/v2 v2.30.0 // indirect | ||||||
|  | 	k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect | ||||||
|  | 	k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect | ||||||
|  | 	sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect | ||||||
|  | 	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect | ||||||
|  | 	sigs.k8s.io/yaml v1.3.0 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| Copyright 2011-2016 Canonical Ltd. | /* | ||||||
|  | Copyright 2022. | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
| @@ -11,3 +12,4 @@ distributed under the License is distributed on an "AS IS" BASIS, | |||||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
| See the License for the specific language governing permissions and | See the License for the specific language governing permissions and | ||||||
| limitations under the License. | limitations under the License. | ||||||
|  | */ | ||||||
							
								
								
									
										254
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,254 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"runtime" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/1Password/connect-sdk-go/connect" | ||||||
|  | 	op "github.com/1Password/onepassword-operator/pkg/onepassword" | ||||||
|  | 	"github.com/1Password/onepassword-operator/pkg/utils" | ||||||
|  | 	"github.com/1Password/onepassword-operator/version" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/cache" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/manager/signals" | ||||||
|  |  | ||||||
|  | 	//	sdkVersion "github.com/operator-framework/operator-sdk/version" | ||||||
|  |  | ||||||
|  | 	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) | ||||||
|  | 	// to ensure that exec-entrypoint and run can make use of them. | ||||||
|  | 	_ "k8s.io/client-go/plugin/pkg/client/auth" | ||||||
|  |  | ||||||
|  | 	k8sruntime "k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||||||
|  | 	clientgoscheme "k8s.io/client-go/kubernetes/scheme" | ||||||
|  | 	ctrl "sigs.k8s.io/controller-runtime" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/healthz" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/log/zap" | ||||||
|  |  | ||||||
|  | 	onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" | ||||||
|  | 	"github.com/1Password/onepassword-operator/controllers" | ||||||
|  | 	//+kubebuilder:scaffold:imports | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	scheme               = k8sruntime.NewScheme() | ||||||
|  | 	setupLog             = ctrl.Log.WithName("setup") | ||||||
|  | 	WatchNamespaceEnvVar = "WATCH_NAMESPACE" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	utilruntime.Must(clientgoscheme.AddToScheme(scheme)) | ||||||
|  |  | ||||||
|  | 	utilruntime.Must(onepasswordv1.AddToScheme(scheme)) | ||||||
|  | 	//+kubebuilder:scaffold:scheme | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func printVersion() { | ||||||
|  | 	setupLog.Info(fmt.Sprintf("Operator Version: %s", version.Version)) | ||||||
|  | 	setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) | ||||||
|  | 	setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) | ||||||
|  | 	// TODO figure out how to get operator-sdk version | ||||||
|  | 	// setupLog.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  | 	var metricsAddr string | ||||||
|  | 	var enableLeaderElection bool | ||||||
|  | 	var probeAddr string | ||||||
|  | 	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") | ||||||
|  | 	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") | ||||||
|  | 	flag.BoolVar(&enableLeaderElection, "leader-elect", false, | ||||||
|  | 		"Enable leader election for controller manager. "+ | ||||||
|  | 			"Enabling this will ensure there is only one active controller manager.") | ||||||
|  | 	opts := zap.Options{ | ||||||
|  | 		Development: true, | ||||||
|  | 	} | ||||||
|  | 	opts.BindFlags(flag.CommandLine) | ||||||
|  | 	flag.Parse() | ||||||
|  |  | ||||||
|  | 	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) | ||||||
|  |  | ||||||
|  | 	printVersion() | ||||||
|  |  | ||||||
|  | 	namespace := os.Getenv(WatchNamespaceEnvVar) | ||||||
|  |  | ||||||
|  | 	options := ctrl.Options{ | ||||||
|  | 		Scheme:                 scheme, | ||||||
|  | 		Namespace:              namespace, | ||||||
|  | 		MetricsBindAddress:     metricsAddr, | ||||||
|  | 		Port:                   9443, | ||||||
|  | 		HealthProbeBindAddress: probeAddr, | ||||||
|  | 		LeaderElection:         enableLeaderElection, | ||||||
|  | 		LeaderElectionID:       "c26807fd.onepassword.com", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2) | ||||||
|  | 	// Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate | ||||||
|  | 	// Also note that you may face performance issues when using this with a high number of namespaces. | ||||||
|  | 	if strings.Contains(namespace, ",") { | ||||||
|  | 		options.Namespace = "" | ||||||
|  | 		options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(namespace, ",")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options) | ||||||
|  | 	if err != nil { | ||||||
|  | 		setupLog.Error(err, "unable to start manager") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Setup One Password Client | ||||||
|  | 	opConnectClient, err := connect.NewClientFromEnvironment() | ||||||
|  | 	if err != nil { | ||||||
|  | 		setupLog.Error(err, "failed to create 1Password client") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err = (&controllers.OnePasswordItemReconciler{ | ||||||
|  | 		Client:          mgr.GetClient(), | ||||||
|  | 		Scheme:          mgr.GetScheme(), | ||||||
|  | 		OpConnectClient: opConnectClient, | ||||||
|  | 	}).SetupWithManager(mgr); err != nil { | ||||||
|  | 		setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	//+kubebuilder:scaffold:builder | ||||||
|  |  | ||||||
|  | 	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { | ||||||
|  | 		setupLog.Error(err, "unable to set up health check") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { | ||||||
|  | 		setupLog.Error(err, "unable to set up ready check") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	setupLog.Info("starting manager") | ||||||
|  | 	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { | ||||||
|  | 		setupLog.Error(err, "problem running manager") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	deploymentNamespace, err := utils.GetOperatorNamespace() | ||||||
|  | 	if err != nil { | ||||||
|  | 		setupLog.Error(err, "Failed to get namespace") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//Setup 1PasswordConnect | ||||||
|  | 	if shouldManageConnect() { | ||||||
|  | 		setupLog.Info("Automated Connect Management Enabled") | ||||||
|  | 		go func() { | ||||||
|  | 			connectStarted := false | ||||||
|  | 			for !connectStarted { | ||||||
|  | 				err := op.SetupConnect(mgr.GetClient(), deploymentNamespace) | ||||||
|  | 				// Cache Not Started is an acceptable error. Retry until cache is started. | ||||||
|  | 				if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { | ||||||
|  | 					setupLog.Error(err, "") | ||||||
|  | 					os.Exit(1) | ||||||
|  | 				} | ||||||
|  | 				if err == nil { | ||||||
|  | 					connectStarted = true | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 	} else { | ||||||
|  | 		setupLog.Info("Automated Connect Management Disabled") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// TODO: Configure Metrics Service. See: https://sdk.operatorframework.io/docs/building-operators/golang/migration/#export-metrics | ||||||
|  |  | ||||||
|  | 	// Setup update secrets task | ||||||
|  | 	updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments()) | ||||||
|  | 	done := make(chan bool) | ||||||
|  | 	ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets()) | ||||||
|  | 	go func() { | ||||||
|  | 		for { | ||||||
|  | 			select { | ||||||
|  | 			case <-done: | ||||||
|  | 				ticker.Stop() | ||||||
|  | 				return | ||||||
|  | 			case <-ticker.C: | ||||||
|  | 				err := updatedSecretsPoller.UpdateKubernetesSecretsTask() | ||||||
|  | 				if err != nil { | ||||||
|  | 					setupLog.Error(err, "error running update kubernetes secret task") | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	// Start the Cmd | ||||||
|  | 	if err := mgr.Start(signals.SetupSignalHandler()); err != nil { | ||||||
|  | 		setupLog.Error(err, "Manager exited non-zero") | ||||||
|  | 		done <- true | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const manageConnect = "MANAGE_CONNECT" | ||||||
|  |  | ||||||
|  | func shouldManageConnect() bool { | ||||||
|  | 	shouldManageConnect, found := os.LookupEnv(manageConnect) | ||||||
|  | 	if found { | ||||||
|  | 		shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			setupLog.Error(err, "") | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		return shouldManageConnectBool | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const envPollingIntervalVariable = "POLLING_INTERVAL" | ||||||
|  | const defaultPollingInterval = 600 | ||||||
|  |  | ||||||
|  | func getPollingIntervalForUpdatingSecrets() time.Duration { | ||||||
|  | 	timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable) | ||||||
|  | 	if found { | ||||||
|  | 		timeInSeconds, err := strconv.Atoi(timeInSecondsString) | ||||||
|  | 		if err == nil { | ||||||
|  | 			return time.Duration(timeInSeconds) * time.Second | ||||||
|  | 		} | ||||||
|  | 		setupLog.Info("Invalid value set for polling interval. Must be a valid integer.") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	setupLog.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval)) | ||||||
|  | 	return time.Duration(defaultPollingInterval) * time.Second | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const restartDeploymentsEnvVariable = "AUTO_RESTART" | ||||||
|  |  | ||||||
|  | func shouldAutoRestartDeployments() bool { | ||||||
|  | 	shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable) | ||||||
|  | 	if found { | ||||||
|  | 		shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			setupLog.Error(err, "") | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		return shouldAutoRestartDeploymentsBool | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| package apis |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	v1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	// Register the types with the Scheme so the components can map objects to GroupVersionKinds and back |  | ||||||
| 	AddToSchemes = append(AddToSchemes, v1.SchemeBuilder.AddToScheme) |  | ||||||
| } |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| package apis |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // AddToSchemes may be used to add all resources defined in the project to a Scheme |  | ||||||
| var AddToSchemes runtime.SchemeBuilder |  | ||||||
|  |  | ||||||
| // AddToScheme adds all Resources to the Scheme |  | ||||||
| func AddToScheme(s *runtime.Scheme) error { |  | ||||||
| 	return AddToSchemes.AddToScheme(s) |  | ||||||
| } |  | ||||||
| @@ -1,6 +0,0 @@ | |||||||
| // Package onepassword contains onepassword API versions. |  | ||||||
| // |  | ||||||
| // This file ensures Go source parsers acknowledge the onepassword package |  | ||||||
| // and any child packages. It can be removed if any other Go source files are |  | ||||||
| // added to this package. |  | ||||||
| package onepassword |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| // Package v1 contains API Schema definitions for the onepassword v1 API group |  | ||||||
| // +k8s:deepcopy-gen=package,register |  | ||||||
| // +groupName=onepassword.com |  | ||||||
| package v1 |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| // NOTE: Boilerplate only.  Ignore this file. |  | ||||||
|  |  | ||||||
| // Package v1 contains API Schema definitions for the onepassword v1 API group |  | ||||||
| // +k8s:deepcopy-gen=package,register |  | ||||||
| // +groupName=onepassword.com |  | ||||||
| package v1 |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"k8s.io/apimachinery/pkg/runtime/schema" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/scheme" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	// SchemeGroupVersion is group version used to register these objects |  | ||||||
| 	SchemeGroupVersion = schema.GroupVersion{Group: "onepassword.com", Version: "v1"} |  | ||||||
|  |  | ||||||
| 	// SchemeBuilder is used to add go types to the GroupVersionKind scheme |  | ||||||
| 	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} |  | ||||||
| ) |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| package controller |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/controller/deployment" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	// AddToManagerFuncs is a list of functions to create controllers and add them to a manager. |  | ||||||
| 	AddToManagerFuncs = append(AddToManagerFuncs, deployment.Add) |  | ||||||
| } |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| package controller |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/controller/onepassworditem" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	// AddToManagerFuncs is a list of functions to create controllers and add them to a manager. |  | ||||||
| 	AddToManagerFuncs = append(AddToManagerFuncs, onepassworditem.Add) |  | ||||||
| } |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| package controller |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/1Password/connect-sdk-go/connect" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/manager" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // AddToManagerFuncs is a list of functions to add all Controllers to the Manager |  | ||||||
| var AddToManagerFuncs []func(manager.Manager, connect.Client) error |  | ||||||
|  |  | ||||||
| // AddToManager adds all Controllers to the Manager |  | ||||||
| func AddToManager(m manager.Manager, opConnectClient connect.Client) error { |  | ||||||
| 	for _, f := range AddToManagerFuncs { |  | ||||||
| 		if err := f(m, opConnectClient); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @@ -1,474 +0,0 @@ | |||||||
| package deployment |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"regexp" |  | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/mocks" |  | ||||||
| 	op "github.com/1Password/onepassword-operator/pkg/onepassword" |  | ||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/onepassword" |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	appsv1 "k8s.io/api/apps/v1" |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	errors2 "k8s.io/apimachinery/pkg/api/errors" |  | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" |  | ||||||
| 	"k8s.io/apimachinery/pkg/types" |  | ||||||
| 	"k8s.io/kubectl/pkg/scheme" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/reconcile" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	deploymentKind       = "Deployment" |  | ||||||
| 	deploymentAPIVersion = "v1" |  | ||||||
| 	name                 = "test-deployment" |  | ||||||
| 	namespace            = "default" |  | ||||||
| 	vaultId              = "hfnjvi6aymbsnfc2xeeoheizda" |  | ||||||
| 	itemId               = "nwrhuano7bcwddcviubpp4mhfq" |  | ||||||
| 	username             = "test-user" |  | ||||||
| 	password             = "QmHumKc$mUeEem7caHtbaBaJ" |  | ||||||
| 	userKey              = "username" |  | ||||||
| 	passKey              = "password" |  | ||||||
| 	version              = 123 |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type testReconcileItem struct { |  | ||||||
| 	testName             string |  | ||||||
| 	deploymentResource   *appsv1.Deployment |  | ||||||
| 	existingSecret       *corev1.Secret |  | ||||||
| 	expectedError        error |  | ||||||
| 	expectedResultSecret *corev1.Secret |  | ||||||
| 	expectedEvents       []string |  | ||||||
| 	opItem               map[string]string |  | ||||||
| 	existingDeployment   *appsv1.Deployment |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	expectedSecretData = map[string][]byte{ |  | ||||||
| 		"password": []byte(password), |  | ||||||
| 		"username": []byte(username), |  | ||||||
| 	} |  | ||||||
| 	itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	time     = metav1.Now() |  | ||||||
| 	regex, _ = regexp.Compile(annotationRegExpString) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var tests = []testReconcileItem{ |  | ||||||
| 	{ |  | ||||||
| 		testName: "Test Delete Deployment where secret is being used in another deployment's volumes", |  | ||||||
| 		deploymentResource: &appsv1.Deployment{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       deploymentKind, |  | ||||||
| 				APIVersion: deploymentAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:              name, |  | ||||||
| 				Namespace:         namespace, |  | ||||||
| 				DeletionTimestamp: &time, |  | ||||||
| 				Finalizers: []string{ |  | ||||||
| 					finalizer, |  | ||||||
| 				}, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.ItemPathAnnotation: itemPath, |  | ||||||
| 					op.NameAnnotation:     name, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingDeployment: &appsv1.Deployment{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       deploymentKind, |  | ||||||
| 				APIVersion: deploymentAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      "another-deployment", |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.ItemPathAnnotation: itemPath, |  | ||||||
| 					op.NameAnnotation:     name, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Spec: appsv1.DeploymentSpec{ |  | ||||||
| 				Template: corev1.PodTemplateSpec{ |  | ||||||
| 					Spec: corev1.PodSpec{ |  | ||||||
| 						Volumes: []corev1.Volume{ |  | ||||||
| 							{ |  | ||||||
| 								Name: name, |  | ||||||
| 								VolumeSource: corev1.VolumeSource{ |  | ||||||
| 									Secret: &corev1.SecretVolumeSource{ |  | ||||||
| 										SecretName: name, |  | ||||||
| 									}, |  | ||||||
| 								}, |  | ||||||
| 							}, |  | ||||||
| 						}, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		expectedError: nil, |  | ||||||
| 		expectedResultSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: username, |  | ||||||
| 			passKey: password, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		testName: "Test Delete Deployment where secret is being used in another deployment's container", |  | ||||||
| 		deploymentResource: &appsv1.Deployment{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       deploymentKind, |  | ||||||
| 				APIVersion: deploymentAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:              name, |  | ||||||
| 				Namespace:         namespace, |  | ||||||
| 				DeletionTimestamp: &time, |  | ||||||
| 				Finalizers: []string{ |  | ||||||
| 					finalizer, |  | ||||||
| 				}, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.ItemPathAnnotation: itemPath, |  | ||||||
| 					op.NameAnnotation:     name, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingDeployment: &appsv1.Deployment{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       deploymentKind, |  | ||||||
| 				APIVersion: deploymentAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      "another-deployment", |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.ItemPathAnnotation: itemPath, |  | ||||||
| 					op.NameAnnotation:     name, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Spec: appsv1.DeploymentSpec{ |  | ||||||
| 				Template: corev1.PodTemplateSpec{ |  | ||||||
| 					Spec: corev1.PodSpec{ |  | ||||||
| 						Containers: []corev1.Container{ |  | ||||||
| 							{ |  | ||||||
| 								Env: []corev1.EnvVar{ |  | ||||||
| 									{ |  | ||||||
| 										Name: name, |  | ||||||
| 										ValueFrom: &corev1.EnvVarSource{ |  | ||||||
| 											SecretKeyRef: &corev1.SecretKeySelector{ |  | ||||||
| 												LocalObjectReference: corev1.LocalObjectReference{ |  | ||||||
| 													Name: name, |  | ||||||
| 												}, |  | ||||||
| 												Key: passKey, |  | ||||||
| 											}, |  | ||||||
| 										}, |  | ||||||
| 									}, |  | ||||||
| 								}, |  | ||||||
| 							}, |  | ||||||
| 						}, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		expectedError: nil, |  | ||||||
| 		expectedResultSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: username, |  | ||||||
| 			passKey: password, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		testName: "Test Delete Deployment", |  | ||||||
| 		deploymentResource: &appsv1.Deployment{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       deploymentKind, |  | ||||||
| 				APIVersion: deploymentAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:              name, |  | ||||||
| 				Namespace:         namespace, |  | ||||||
| 				DeletionTimestamp: &time, |  | ||||||
| 				Finalizers: []string{ |  | ||||||
| 					finalizer, |  | ||||||
| 				}, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.ItemPathAnnotation: itemPath, |  | ||||||
| 					op.NameAnnotation:     name, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		expectedError:        nil, |  | ||||||
| 		expectedResultSecret: nil, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: username, |  | ||||||
| 			passKey: password, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		testName: "Test Do not update if OnePassword Item Version has not changed", |  | ||||||
| 		deploymentResource: &appsv1.Deployment{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       deploymentKind, |  | ||||||
| 				APIVersion: deploymentAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.ItemPathAnnotation: itemPath, |  | ||||||
| 					op.NameAnnotation:     name, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		expectedError: nil, |  | ||||||
| 		expectedResultSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: "data we don't expect to have updated", |  | ||||||
| 			passKey: "data we don't expect to have updated", |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		testName: "Test Updating Existing Kubernetes Secret using Deployment", |  | ||||||
| 		deploymentResource: &appsv1.Deployment{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       deploymentKind, |  | ||||||
| 				APIVersion: deploymentAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.ItemPathAnnotation: itemPath, |  | ||||||
| 					op.NameAnnotation:     name, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: "456", |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		expectedError: nil, |  | ||||||
| 		expectedResultSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: username, |  | ||||||
| 			passKey: password, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		testName: "Create Deployment", |  | ||||||
| 		deploymentResource: &appsv1.Deployment{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       deploymentKind, |  | ||||||
| 				APIVersion: deploymentAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.ItemPathAnnotation: itemPath, |  | ||||||
| 					op.NameAnnotation:     name, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: nil, |  | ||||||
| 		expectedError:  nil, |  | ||||||
| 		expectedResultSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: username, |  | ||||||
| 			passKey: password, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestReconcileDepoyment(t *testing.T) { |  | ||||||
| 	for _, testData := range tests { |  | ||||||
| 		t.Run(testData.testName, func(t *testing.T) { |  | ||||||
|  |  | ||||||
| 			// Register operator types with the runtime scheme. |  | ||||||
| 			s := scheme.Scheme |  | ||||||
| 			s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.deploymentResource) |  | ||||||
|  |  | ||||||
| 			// Objects to track in the fake client. |  | ||||||
| 			objs := []runtime.Object{ |  | ||||||
| 				testData.deploymentResource, |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if testData.existingSecret != nil { |  | ||||||
| 				objs = append(objs, testData.existingSecret) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if testData.existingDeployment != nil { |  | ||||||
| 				objs = append(objs, testData.existingDeployment) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Create a fake client to mock API calls. |  | ||||||
| 			cl := fake.NewFakeClientWithScheme(s, objs...) |  | ||||||
| 			// Create a Deployment object with the scheme and mock  kubernetes |  | ||||||
| 			// and 1Password Connect client. |  | ||||||
|  |  | ||||||
| 			opConnectClient := &mocks.TestClient{} |  | ||||||
| 			mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { |  | ||||||
|  |  | ||||||
| 				item := onepassword.Item{} |  | ||||||
| 				item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) |  | ||||||
| 				item.Version = version |  | ||||||
| 				item.Vault.ID = vaultUUID |  | ||||||
| 				item.ID = uuid |  | ||||||
| 				return &item, nil |  | ||||||
| 			} |  | ||||||
| 			r := &ReconcileDeployment{ |  | ||||||
| 				kubeClient:         cl, |  | ||||||
| 				scheme:             s, |  | ||||||
| 				opConnectClient:    opConnectClient, |  | ||||||
| 				opAnnotationRegExp: regex, |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Mock request to simulate Reconcile() being called on an event for a |  | ||||||
| 			// watched resource . |  | ||||||
| 			req := reconcile.Request{ |  | ||||||
| 				NamespacedName: types.NamespacedName{ |  | ||||||
| 					Name:      name, |  | ||||||
| 					Namespace: namespace, |  | ||||||
| 				}, |  | ||||||
| 			} |  | ||||||
| 			_, err := r.Reconcile(req) |  | ||||||
|  |  | ||||||
| 			assert.Equal(t, testData.expectedError, err) |  | ||||||
|  |  | ||||||
| 			var expectedSecretName string |  | ||||||
| 			if testData.expectedResultSecret == nil { |  | ||||||
| 				expectedSecretName = testData.deploymentResource.Name |  | ||||||
| 			} else { |  | ||||||
| 				expectedSecretName = testData.expectedResultSecret.Name |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Check if Secret has been created and has the correct data |  | ||||||
| 			secret := &corev1.Secret{} |  | ||||||
| 			err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret) |  | ||||||
|  |  | ||||||
| 			if testData.expectedResultSecret == nil { |  | ||||||
| 				assert.Error(t, err) |  | ||||||
| 				assert.True(t, errors2.IsNotFound(err)) |  | ||||||
| 			} else { |  | ||||||
| 				assert.Equal(t, testData.expectedResultSecret.Data, secret.Data) |  | ||||||
| 				assert.Equal(t, testData.expectedResultSecret.Name, secret.Name) |  | ||||||
| 				assert.Equal(t, testData.expectedResultSecret.Type, secret.Type) |  | ||||||
| 				assert.Equal(t, testData.expectedResultSecret.Annotations[op.VersionAnnotation], secret.Annotations[op.VersionAnnotation]) |  | ||||||
|  |  | ||||||
| 				updatedCR := &appsv1.Deployment{} |  | ||||||
| 				err = cl.Get(context.TODO(), req.NamespacedName, updatedCR) |  | ||||||
| 				assert.NoError(t, err) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func generateFields(username, password string) []*onepassword.ItemField { |  | ||||||
| 	fields := []*onepassword.ItemField{ |  | ||||||
| 		{ |  | ||||||
| 			Label: "username", |  | ||||||
| 			Value: username, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Label: "password", |  | ||||||
| 			Value: password, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	return fields |  | ||||||
| } |  | ||||||
| @@ -1,153 +0,0 @@ | |||||||
| package onepassworditem |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
|  |  | ||||||
| 	onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" |  | ||||||
| 	kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" |  | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/onepassword" |  | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/utils" |  | ||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/connect" |  | ||||||
|  |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/api/errors" |  | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" |  | ||||||
| 	ctrl "sigs.k8s.io/controller-runtime" |  | ||||||
| 	kubeClient "sigs.k8s.io/controller-runtime/pkg/client" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/controller" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/handler" |  | ||||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/manager" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/reconcile" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/source" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var log = logf.Log.WithName("controller_onepassworditem") |  | ||||||
| var finalizer = "onepassword.com/finalizer.secret" |  | ||||||
|  |  | ||||||
| func Add(mgr manager.Manager, opConnectClient connect.Client) error { |  | ||||||
| 	return add(mgr, newReconciler(mgr, opConnectClient)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func newReconciler(mgr manager.Manager, opConnectClient connect.Client) *ReconcileOnePasswordItem { |  | ||||||
| 	return &ReconcileOnePasswordItem{ |  | ||||||
| 		kubeClient:      mgr.GetClient(), |  | ||||||
| 		scheme:          mgr.GetScheme(), |  | ||||||
| 		opConnectClient: opConnectClient, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func add(mgr manager.Manager, r reconcile.Reconciler) error { |  | ||||||
| 	c, err := controller.New("onepassworditem-controller", mgr, controller.Options{Reconciler: r}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Watch for changes to primary resource OnePasswordItem |  | ||||||
| 	err = c.Watch(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var _ reconcile.Reconciler = &ReconcileOnePasswordItem{} |  | ||||||
|  |  | ||||||
| type ReconcileOnePasswordItem struct { |  | ||||||
| 	kubeClient      kubeClient.Client |  | ||||||
| 	scheme          *runtime.Scheme |  | ||||||
| 	opConnectClient connect.Client |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (r *ReconcileOnePasswordItem) SetupWithManager(mgr ctrl.Manager) error { |  | ||||||
| 	return ctrl.NewControllerManagedBy(mgr). |  | ||||||
| 		For(&onepasswordv1.OnePasswordItem{}). |  | ||||||
| 		Complete(r) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (r *ReconcileOnePasswordItem) Reconcile(request reconcile.Request) (reconcile.Result, error) { |  | ||||||
| 	reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) |  | ||||||
| 	reqLogger.Info("Reconciling OnePasswordItem") |  | ||||||
|  |  | ||||||
| 	onepassworditem := &onepasswordv1.OnePasswordItem{} |  | ||||||
| 	err := r.kubeClient.Get(context.Background(), request.NamespacedName, onepassworditem) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if errors.IsNotFound(err) { |  | ||||||
| 			return reconcile.Result{}, nil |  | ||||||
| 		} |  | ||||||
| 		return reconcile.Result{}, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// If the deployment is not being deleted |  | ||||||
| 	if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() { |  | ||||||
| 		// Adds a finalizer to the deployment if one does not exist. |  | ||||||
| 		// This is so we can handle cleanup of associated secrets properly |  | ||||||
| 		if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { |  | ||||||
| 			onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer) |  | ||||||
| 			if err := r.kubeClient.Update(context.Background(), onepassworditem); err != nil { |  | ||||||
| 				return reconcile.Result{}, err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Handles creation or updating secrets for deployment if needed |  | ||||||
| 		if err := r.HandleOnePasswordItem(onepassworditem, request); err != nil { |  | ||||||
| 			return reconcile.Result{}, err |  | ||||||
| 		} |  | ||||||
| 		return reconcile.Result{}, nil |  | ||||||
| 	} |  | ||||||
| 	// If one password finalizer exists then we must cleanup associated secrets |  | ||||||
| 	if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { |  | ||||||
|  |  | ||||||
| 		// Delete associated kubernetes secret |  | ||||||
| 		if err = r.cleanupKubernetesSecret(onepassworditem); err != nil { |  | ||||||
| 			return reconcile.Result{}, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Remove finalizer now that cleanup is complete |  | ||||||
| 		if err := r.removeFinalizer(onepassworditem); err != nil { |  | ||||||
| 			return reconcile.Result{}, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return reconcile.Result{}, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (r *ReconcileOnePasswordItem) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error { |  | ||||||
| 	onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer) |  | ||||||
| 	if err := r.kubeClient.Update(context.Background(), onePasswordItem); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (r *ReconcileOnePasswordItem) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error { |  | ||||||
| 	kubernetesSecret := &corev1.Secret{} |  | ||||||
| 	kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name |  | ||||||
| 	kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace |  | ||||||
|  |  | ||||||
| 	r.kubeClient.Delete(context.Background(), kubernetesSecret) |  | ||||||
| 	if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil { |  | ||||||
| 		if !errors.IsNotFound(err) { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem(opSecret *onepasswordv1.OnePasswordItem) error { |  | ||||||
| 	opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer) |  | ||||||
| 	return r.kubeClient.Update(context.Background(), opSecret) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error { |  | ||||||
| 	secretName := resource.GetName() |  | ||||||
|  |  | ||||||
| 	item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("Failed to retrieve item: %v", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item) |  | ||||||
| } |  | ||||||
| @@ -1,308 +0,0 @@ | |||||||
| package onepassworditem |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/mocks" |  | ||||||
| 	op "github.com/1Password/onepassword-operator/pkg/onepassword" |  | ||||||
|  |  | ||||||
| 	onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" |  | ||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/onepassword" |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	errors2 "k8s.io/apimachinery/pkg/api/errors" |  | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" |  | ||||||
| 	"k8s.io/apimachinery/pkg/types" |  | ||||||
| 	"k8s.io/kubectl/pkg/scheme" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/reconcile" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	onePasswordItemKind       = "OnePasswordItem" |  | ||||||
| 	onePasswordItemAPIVersion = "onepassword.com/v1" |  | ||||||
| 	name                      = "test" |  | ||||||
| 	namespace                 = "default" |  | ||||||
| 	vaultId                   = "hfnjvi6aymbsnfc2xeeoheizda" |  | ||||||
| 	itemId                    = "nwrhuano7bcwddcviubpp4mhfq" |  | ||||||
| 	username                  = "test-user" |  | ||||||
| 	password                  = "QmHumKc$mUeEem7caHtbaBaJ" |  | ||||||
| 	userKey                   = "username" |  | ||||||
| 	passKey                   = "password" |  | ||||||
| 	version                   = 123 |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type testReconcileItem struct { |  | ||||||
| 	testName                string |  | ||||||
| 	customResource          *onepasswordv1.OnePasswordItem |  | ||||||
| 	existingSecret          *corev1.Secret |  | ||||||
| 	expectedError           error |  | ||||||
| 	expectedResultSecret    *corev1.Secret |  | ||||||
| 	expectedEvents          []string |  | ||||||
| 	opItem                  map[string]string |  | ||||||
| 	existingOnePasswordItem *onepasswordv1.OnePasswordItem |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	expectedSecretData = map[string][]byte{ |  | ||||||
| 		"password": []byte(password), |  | ||||||
| 		"username": []byte(username), |  | ||||||
| 	} |  | ||||||
| 	itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	time = metav1.Now() |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var tests = []testReconcileItem{ |  | ||||||
| 	{ |  | ||||||
| 		testName: "Test Delete OnePasswordItem", |  | ||||||
| 		customResource: &onepasswordv1.OnePasswordItem{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       onePasswordItemKind, |  | ||||||
| 				APIVersion: onePasswordItemAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:              name, |  | ||||||
| 				Namespace:         namespace, |  | ||||||
| 				DeletionTimestamp: &time, |  | ||||||
| 				Finalizers: []string{ |  | ||||||
| 					finalizer, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Spec: onepasswordv1.OnePasswordItemSpec{ |  | ||||||
| 				ItemPath: itemPath, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		expectedError:        nil, |  | ||||||
| 		expectedResultSecret: nil, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: username, |  | ||||||
| 			passKey: password, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		testName: "Test Do not update if OnePassword Version has not changed", |  | ||||||
| 		customResource: &onepasswordv1.OnePasswordItem{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       onePasswordItemKind, |  | ||||||
| 				APIVersion: onePasswordItemAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 			}, |  | ||||||
| 			Spec: onepasswordv1.OnePasswordItemSpec{ |  | ||||||
| 				ItemPath: itemPath, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		expectedError: nil, |  | ||||||
| 		expectedResultSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: "data we don't expect to have updated", |  | ||||||
| 			passKey: "data we don't expect to have updated", |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		testName: "Test Updating Existing Kubernetes Secret using OnePasswordItem", |  | ||||||
| 		customResource: &onepasswordv1.OnePasswordItem{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       onePasswordItemKind, |  | ||||||
| 				APIVersion: onePasswordItemAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 			}, |  | ||||||
| 			Spec: onepasswordv1.OnePasswordItemSpec{ |  | ||||||
| 				ItemPath: itemPath, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: "456", |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		expectedError: nil, |  | ||||||
| 		expectedResultSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: username, |  | ||||||
| 			passKey: password, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		testName: "Custom secret type", |  | ||||||
| 		customResource: &onepasswordv1.OnePasswordItem{ |  | ||||||
| 			TypeMeta: metav1.TypeMeta{ |  | ||||||
| 				Kind:       onePasswordItemKind, |  | ||||||
| 				APIVersion: onePasswordItemAPIVersion, |  | ||||||
| 			}, |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 			}, |  | ||||||
| 			Spec: onepasswordv1.OnePasswordItemSpec{ |  | ||||||
| 				ItemPath: itemPath, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		existingSecret: nil, |  | ||||||
| 		expectedError:  nil, |  | ||||||
| 		expectedResultSecret: &corev1.Secret{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      name, |  | ||||||
| 				Namespace: namespace, |  | ||||||
| 				Annotations: map[string]string{ |  | ||||||
| 					op.VersionAnnotation: fmt.Sprint(version), |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			Data: expectedSecretData, |  | ||||||
| 		}, |  | ||||||
| 		opItem: map[string]string{ |  | ||||||
| 			userKey: username, |  | ||||||
| 			passKey: password, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestReconcileOnePasswordItem(t *testing.T) { |  | ||||||
| 	for _, testData := range tests { |  | ||||||
| 		t.Run(testData.testName, func(t *testing.T) { |  | ||||||
|  |  | ||||||
| 			// Register operator types with the runtime scheme. |  | ||||||
| 			s := scheme.Scheme |  | ||||||
| 			s.AddKnownTypes(onepasswordv1.SchemeGroupVersion, testData.customResource) |  | ||||||
|  |  | ||||||
| 			// Objects to track in the fake client. |  | ||||||
| 			objs := []runtime.Object{ |  | ||||||
| 				testData.customResource, |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if testData.existingSecret != nil { |  | ||||||
| 				objs = append(objs, testData.existingSecret) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if testData.existingOnePasswordItem != nil { |  | ||||||
| 				objs = append(objs, testData.existingOnePasswordItem) |  | ||||||
| 			} |  | ||||||
| 			// Create a fake client to mock API calls. |  | ||||||
| 			cl := fake.NewFakeClientWithScheme(s, objs...) |  | ||||||
| 			// Create a OnePasswordItem object with the scheme and mock  kubernetes |  | ||||||
| 			// and 1Password Connect client. |  | ||||||
|  |  | ||||||
| 			opConnectClient := &mocks.TestClient{} |  | ||||||
| 			mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { |  | ||||||
|  |  | ||||||
| 				item := onepassword.Item{} |  | ||||||
| 				item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) |  | ||||||
| 				item.Version = version |  | ||||||
| 				item.Vault.ID = vaultUUID |  | ||||||
| 				item.ID = uuid |  | ||||||
| 				return &item, nil |  | ||||||
| 			} |  | ||||||
| 			r := &ReconcileOnePasswordItem{ |  | ||||||
| 				kubeClient:      cl, |  | ||||||
| 				scheme:          s, |  | ||||||
| 				opConnectClient: opConnectClient, |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Mock request to simulate Reconcile() being called on an event for a |  | ||||||
| 			// watched resource . |  | ||||||
| 			req := reconcile.Request{ |  | ||||||
| 				NamespacedName: types.NamespacedName{ |  | ||||||
| 					Name:      name, |  | ||||||
| 					Namespace: namespace, |  | ||||||
| 				}, |  | ||||||
| 			} |  | ||||||
| 			_, err := r.Reconcile(req) |  | ||||||
|  |  | ||||||
| 			assert.Equal(t, testData.expectedError, err) |  | ||||||
|  |  | ||||||
| 			var expectedSecretName string |  | ||||||
| 			if testData.expectedResultSecret == nil { |  | ||||||
| 				expectedSecretName = testData.customResource.Name |  | ||||||
| 			} else { |  | ||||||
| 				expectedSecretName = testData.expectedResultSecret.Name |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// Check if Secret has been created and has the correct data |  | ||||||
| 			secret := &corev1.Secret{} |  | ||||||
| 			err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret) |  | ||||||
|  |  | ||||||
| 			if testData.expectedResultSecret == nil { |  | ||||||
| 				assert.Error(t, err) |  | ||||||
| 				assert.True(t, errors2.IsNotFound(err)) |  | ||||||
| 			} else { |  | ||||||
| 				assert.Equal(t, testData.expectedResultSecret.Data, secret.Data) |  | ||||||
| 				assert.Equal(t, testData.expectedResultSecret.Name, secret.Name) |  | ||||||
| 				assert.Equal(t, testData.expectedResultSecret.Type, secret.Type) |  | ||||||
| 				assert.Equal(t, testData.expectedResultSecret.Annotations[op.VersionAnnotation], secret.Annotations[op.VersionAnnotation]) |  | ||||||
|  |  | ||||||
| 				updatedCR := &onepasswordv1.OnePasswordItem{} |  | ||||||
| 				err = cl.Get(context.TODO(), req.NamespacedName, updatedCR) |  | ||||||
| 				assert.NoError(t, err) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func generateFields(username, password string) []*onepassword.ItemField { |  | ||||||
| 	fields := []*onepassword.ItemField{ |  | ||||||
| 		{ |  | ||||||
| 			Label: "username", |  | ||||||
| 			Value: username, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Label: "password", |  | ||||||
| 			Value: password, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	return fields |  | ||||||
| } |  | ||||||
| @@ -4,31 +4,60 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"reflect" | ||||||
|  |  | ||||||
|  | 	errs "errors" | ||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | 	"github.com/1Password/connect-sdk-go/onepassword" | ||||||
|  |  | ||||||
|  | 	"github.com/1Password/onepassword-operator/pkg/utils" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/api/errors" | 	"k8s.io/apimachinery/pkg/api/errors" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	kubeValidate "k8s.io/apimachinery/pkg/util/validation" | ||||||
|  |  | ||||||
| 	kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" | 	kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" | 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const onepasswordPrefix = "onepasswordoperator" | const OnepasswordPrefix = "operator.1password.io" | ||||||
| const NameAnnotation = onepasswordPrefix + "/item-name" | const NameAnnotation = OnepasswordPrefix + "/item-name" | ||||||
| const VersionAnnotation = onepasswordPrefix + "/item-version" | const VersionAnnotation = OnepasswordPrefix + "/item-version" | ||||||
| const restartAnnotation = onepasswordPrefix + "/lastRestarted" | const restartAnnotation = OnepasswordPrefix + "/last-restarted" | ||||||
| const ItemPathAnnotation = onepasswordPrefix + "/item-path" | const ItemPathAnnotation = OnepasswordPrefix + "/item-path" | ||||||
|  | const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" | ||||||
|  |  | ||||||
|  | var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable") | ||||||
|  |  | ||||||
| var log = logf.Log | var log = logf.Log | ||||||
|  |  | ||||||
| func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item) error { | func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string, ownerRef *metav1.OwnerReference) error { | ||||||
|  |  | ||||||
| 	itemVersion := fmt.Sprint(item.Version) | 	itemVersion := fmt.Sprint(item.Version) | ||||||
| 	annotations := map[string]string{ |  | ||||||
| 		VersionAnnotation:  itemVersion, | 	// If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map | ||||||
| 		ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID), | 	if secretAnnotations == nil { | ||||||
|  | 		secretAnnotations = map[string]string{} | ||||||
| 	} | 	} | ||||||
| 	secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, annotations, *item) |  | ||||||
|  | 	secretAnnotations[VersionAnnotation] = itemVersion | ||||||
|  | 	secretAnnotations[ItemPathAnnotation] = fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID) | ||||||
|  |  | ||||||
|  | 	if autoRestart != "" { | ||||||
|  | 		_, err := utils.StringToBool(autoRestart) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		secretAnnotations[RestartDeploymentsAnnotation] = autoRestart | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// "Opaque" and "" secret types are treated the same by Kubernetes. | ||||||
|  | 	secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item, ownerRef) | ||||||
|  |  | ||||||
| 	currentSecret := &corev1.Secret{} | 	currentSecret := &corev1.Secret{} | ||||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) | 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) | ||||||
| @@ -39,9 +68,17 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if currentSecret.Annotations[VersionAnnotation] != itemVersion { | 	currentAnnotations := currentSecret.Annotations | ||||||
|  | 	currentLabels := currentSecret.Labels | ||||||
|  | 	currentSecretType := string(currentSecret.Type) | ||||||
|  | 	if !reflect.DeepEqual(currentSecretType, secretType) { | ||||||
|  | 		return ErrCannotUpdateSecretType | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) { | ||||||
| 		log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) | 		log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) | ||||||
| 		currentSecret.ObjectMeta.Annotations = annotations | 		currentSecret.ObjectMeta.Annotations = secretAnnotations | ||||||
|  | 		currentSecret.ObjectMeta.Labels = labels | ||||||
| 		currentSecret.Data = secret.Data | 		currentSecret.Data = secret.Data | ||||||
| 		return kubeClient.Update(context.Background(), currentSecret) | 		return kubeClient.Update(context.Background(), currentSecret) | ||||||
| 	} | 	} | ||||||
| @@ -50,23 +87,99 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, item onepassword.Item) *corev1.Secret { | func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret { | ||||||
|  | 	var ownerRefs []metav1.OwnerReference | ||||||
|  | 	if ownerRef != nil { | ||||||
|  | 		ownerRefs = []metav1.OwnerReference{*ownerRef} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return &corev1.Secret{ | 	return &corev1.Secret{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
| 			Name:        name, | 			Name:            formatSecretName(name), | ||||||
| 			Namespace:   namespace, | 			Namespace:       namespace, | ||||||
| 			Annotations: annotations, | 			Annotations:     annotations, | ||||||
|  | 			Labels:          labels, | ||||||
|  | 			OwnerReferences: ownerRefs, | ||||||
| 		}, | 		}, | ||||||
| 		Data: BuildKubernetesSecretData(item.Fields), | 		Data: BuildKubernetesSecretData(item.Fields, item.Files), | ||||||
|  | 		Type: corev1.SecretType(secretType), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byte { | func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte { | ||||||
| 	secretData := map[string][]byte{} | 	secretData := map[string][]byte{} | ||||||
| 	for i := 0; i < len(fields); i++ { | 	for i := 0; i < len(fields); i++ { | ||||||
| 		if fields[i].Value != "" { | 		if fields[i].Value != "" { | ||||||
| 			secretData[fields[i].Label] = []byte(fields[i].Value) | 			key := formatSecretDataName(fields[i].Label) | ||||||
|  | 			secretData[key] = []byte(fields[i].Value) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// populate unpopulated fields from files | ||||||
|  | 	for _, file := range files { | ||||||
|  | 		content, err := file.Content() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error(err, "Could not load contents of file %s", file.Name) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if content != nil { | ||||||
|  | 			key := file.Name | ||||||
|  | 			if secretData[key] == nil { | ||||||
|  | 				secretData[key] = content | ||||||
|  | 			} else { | ||||||
|  | 				log.Info(fmt.Sprintf("File '%s' ignored because of a field with the same name", file.Name)) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return secretData | 	return secretData | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // formatSecretName rewrites a value to be a valid Secret name. | ||||||
|  | // | ||||||
|  | // The Secret meta.name and data keys must be valid DNS subdomain names | ||||||
|  | // (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) | ||||||
|  | func formatSecretName(value string) string { | ||||||
|  | 	if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 { | ||||||
|  | 		return value | ||||||
|  | 	} | ||||||
|  | 	return createValidSecretName(value) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // formatSecretDataName rewrites a value to be a valid Secret data key. | ||||||
|  | // | ||||||
|  | // The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.` | ||||||
|  | // (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) | ||||||
|  | func formatSecretDataName(value string) string { | ||||||
|  | 	if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 { | ||||||
|  | 		return value | ||||||
|  | 	} | ||||||
|  | 	return createValidSecretDataName(value) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+") | ||||||
|  |  | ||||||
|  | func createValidSecretName(value string) string { | ||||||
|  | 	result := strings.ToLower(value) | ||||||
|  | 	result = invalidDNS1123Chars.ReplaceAllString(result, "-") | ||||||
|  |  | ||||||
|  | 	if len(result) > kubeValidate.DNS1123SubdomainMaxLength { | ||||||
|  | 		result = result[0:kubeValidate.DNS1123SubdomainMaxLength] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// first and last character MUST be alphanumeric | ||||||
|  | 	return strings.Trim(result, "-.") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+") | ||||||
|  | var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)") | ||||||
|  |  | ||||||
|  | func createValidSecretDataName(value string) string { | ||||||
|  | 	result := invalidStartEndChars.ReplaceAllString(value, "") | ||||||
|  | 	result = invalidDataChars.ReplaceAllString(result, "-") | ||||||
|  |  | ||||||
|  | 	if len(result) > kubeValidate.DNS1123SubdomainMaxLength { | ||||||
|  | 		result = result[0:kubeValidate.DNS1123SubdomainMaxLength] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|   | |||||||
| @@ -8,11 +8,15 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | 	"github.com/1Password/connect-sdk-go/onepassword" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	kubeValidate "k8s.io/apimachinery/pkg/util/validation" | ||||||
| 	"k8s.io/client-go/kubernetes" | 	"k8s.io/client-go/kubernetes" | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const restartDeploymentAnnotation = "false" | ||||||
|  |  | ||||||
| type k8s struct { | type k8s struct { | ||||||
| 	clientset kubernetes.Interface | 	clientset kubernetes.Interface | ||||||
| } | } | ||||||
| @@ -28,7 +32,13 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||||
|  |  | ||||||
| 	kubeClient := fake.NewFakeClient() | 	kubeClient := fake.NewFakeClient() | ||||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item) | 	secretLabels := map[string]string{} | ||||||
|  | 	secretAnnotations := map[string]string{ | ||||||
|  | 		"testAnnotation": "exists", | ||||||
|  | 	} | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
|  | 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Unexpected error: %v", err) | 		t.Errorf("Unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -40,6 +50,58 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 	compareFields(item.Fields, createdSecret.Data, t) | 	compareFields(item.Fields, createdSecret.Data, t) | ||||||
| 	compareAnnotationsToItem(createdSecret.Annotations, item, t) | 	compareAnnotationsToItem(createdSecret.Annotations, item, t) | ||||||
|  |  | ||||||
|  | 	if createdSecret.Annotations["testAnnotation"] != "exists" { | ||||||
|  | 		t.Errorf("Expected testAnnotation to be merged with existing annotations, but wasn't.") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) { | ||||||
|  | 	secretName := "test-secret-name" | ||||||
|  | 	namespace := "test" | ||||||
|  |  | ||||||
|  | 	item := onepassword.Item{} | ||||||
|  | 	item.Fields = generateFields(5) | ||||||
|  | 	item.Version = 123 | ||||||
|  | 	item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | ||||||
|  | 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||||
|  |  | ||||||
|  | 	kubeClient := fake.NewFakeClient() | ||||||
|  | 	secretLabels := map[string]string{} | ||||||
|  | 	secretAnnotations := map[string]string{ | ||||||
|  | 		"testAnnotation": "exists", | ||||||
|  | 	} | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
|  | 	ownerRef := &metav1.OwnerReference{ | ||||||
|  | 		Kind:       "Deployment", | ||||||
|  | 		APIVersion: "apps/v1", | ||||||
|  | 		Name:       "test-deployment", | ||||||
|  | 		UID:        types.UID("test-uid"), | ||||||
|  | 	} | ||||||
|  | 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, ownerRef) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Unexpected error: %v", err) | ||||||
|  | 	} | ||||||
|  | 	createdSecret := &corev1.Secret{} | ||||||
|  | 	err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) | ||||||
|  |  | ||||||
|  | 	// Check owner references. | ||||||
|  | 	gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences | ||||||
|  | 	if len(gotOwnerRefs) != 1 { | ||||||
|  | 		t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	expOwnerRef := metav1.OwnerReference{ | ||||||
|  | 		Kind:       "Deployment", | ||||||
|  | 		APIVersion: "apps/v1", | ||||||
|  | 		Name:       "test-deployment", | ||||||
|  | 		UID:        types.UID("test-uid"), | ||||||
|  | 	} | ||||||
|  | 	gotOwnerRef := gotOwnerRefs[0] | ||||||
|  | 	if gotOwnerRef != expOwnerRef { | ||||||
|  | 		t.Errorf("Expected owner reference value: %v but got: %v", expOwnerRef, gotOwnerRef) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||||
| @@ -53,7 +115,12 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||||
|  |  | ||||||
| 	kubeClient := fake.NewFakeClient() | 	kubeClient := fake.NewFakeClient() | ||||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item) | 	secretLabels := map[string]string{} | ||||||
|  | 	secretAnnotations := map[string]string{} | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
|  | 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Unexpected error: %v", err) | 		t.Errorf("Unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -64,7 +131,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	newItem.Version = 456 | 	newItem.Version = 456 | ||||||
| 	newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | 	newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | ||||||
| 	newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" | 	newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||||
| 	err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem) | 	err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Unexpected error: %v", err) | 		t.Errorf("Unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -80,7 +147,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| func TestBuildKubernetesSecretData(t *testing.T) { | func TestBuildKubernetesSecretData(t *testing.T) { | ||||||
| 	fields := generateFields(5) | 	fields := generateFields(5) | ||||||
|  |  | ||||||
| 	secretData := BuildKubernetesSecretData(fields) | 	secretData := BuildKubernetesSecretData(fields, nil) | ||||||
| 	if len(secretData) != len(fields) { | 	if len(secretData) != len(fields) { | ||||||
| 		t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData)) | 		t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData)) | ||||||
| 	} | 	} | ||||||
| @@ -97,9 +164,11 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 	item := onepassword.Item{} | 	item := onepassword.Item{} | ||||||
| 	item.Fields = generateFields(5) | 	item.Fields = generateFields(5) | ||||||
|  | 	labels := map[string]string{} | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
| 	kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item) | 	kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil) | ||||||
| 	if kubeSecret.Name != name { | 	if kubeSecret.Name != strings.ToLower(name) { | ||||||
| 		t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) | 		t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) | ||||||
| 	} | 	} | ||||||
| 	if kubeSecret.Namespace != namespace { | 	if kubeSecret.Namespace != namespace { | ||||||
| @@ -111,6 +180,79 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	compareFields(item.Fields, kubeSecret.Data, t) | 	compareFields(item.Fields, kubeSecret.Data, t) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) { | ||||||
|  | 	name := "inV@l1d k8s secret%name" | ||||||
|  | 	expectedName := "inv-l1d-k8s-secret-name" | ||||||
|  | 	namespace := "someNamespace" | ||||||
|  | 	annotations := map[string]string{ | ||||||
|  | 		"annotationKey": "annotationValue", | ||||||
|  | 	} | ||||||
|  | 	labels := map[string]string{} | ||||||
|  | 	item := onepassword.Item{} | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
|  | 	item.Fields = []*onepassword.ItemField{ | ||||||
|  | 		{ | ||||||
|  | 			Label: "label w%th invalid ch!rs-", | ||||||
|  | 			Value: "value1", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Label: strings.Repeat("x", kubeValidate.DNS1123SubdomainMaxLength+1), | ||||||
|  | 			Value: "name exceeds max length", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil) | ||||||
|  |  | ||||||
|  | 	// Assert Secret's meta.name was fixed | ||||||
|  | 	if kubeSecret.Name != expectedName { | ||||||
|  | 		t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) | ||||||
|  | 	} | ||||||
|  | 	if kubeSecret.Namespace != namespace { | ||||||
|  | 		t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// assert labels were fixed for each data key | ||||||
|  | 	for key := range kubeSecret.Data { | ||||||
|  | 		if !validLabel(key) { | ||||||
|  | 			t.Errorf("Expected valid kubernetes label, got %s", key) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) { | ||||||
|  | 	secretName := "tls-test-secret-name" | ||||||
|  | 	namespace := "test" | ||||||
|  |  | ||||||
|  | 	item := onepassword.Item{} | ||||||
|  | 	item.Fields = generateFields(5) | ||||||
|  | 	item.Version = 123 | ||||||
|  | 	item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | ||||||
|  | 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||||
|  |  | ||||||
|  | 	kubeClient := fake.NewFakeClient() | ||||||
|  | 	secretLabels := map[string]string{} | ||||||
|  | 	secretAnnotations := map[string]string{ | ||||||
|  | 		"testAnnotation": "exists", | ||||||
|  | 	} | ||||||
|  | 	secretType := "kubernetes.io/tls" | ||||||
|  |  | ||||||
|  | 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Unexpected error: %v", err) | ||||||
|  | 	} | ||||||
|  | 	createdSecret := &corev1.Secret{} | ||||||
|  | 	err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Secret was not created: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if createdSecret.Type != corev1.SecretTypeTLS { | ||||||
|  | 		t.Errorf("Expected secretType to be of tyype corev1.SecretTypeTLS, got %s", string(createdSecret.Type)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { | func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { | ||||||
| 	actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) | 	actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -125,6 +267,10 @@ func compareAnnotationsToItem(annotations map[string]string, item onepassword.It | |||||||
| 	if annotations[VersionAnnotation] != fmt.Sprint(item.Version) { | 	if annotations[VersionAnnotation] != fmt.Sprint(item.Version) { | ||||||
| 		t.Errorf("Expected annotation version to be %v but was %v", item.Version, annotations[VersionAnnotation]) | 		t.Errorf("Expected annotation version to be %v but was %v", item.Version, annotations[VersionAnnotation]) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if annotations[RestartDeploymentsAnnotation] != "false" { | ||||||
|  | 		t.Errorf("Expected restart deployments annotation to be %v but was %v", restartDeploymentAnnotation, RestartDeploymentsAnnotation) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func compareFields(actualFields []*onepassword.ItemField, secretData map[string][]byte, t *testing.T) { | func compareFields(actualFields []*onepassword.ItemField, secretData map[string][]byte, t *testing.T) { | ||||||
| @@ -158,3 +304,10 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) { | |||||||
| 	} | 	} | ||||||
| 	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) | 	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) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func validLabel(v string) bool { | ||||||
|  | 	if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|   | |||||||
| @@ -5,23 +5,33 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type TestClient struct { | type TestClient struct { | ||||||
| 	GetVaultsFunc      func() ([]onepassword.Vault, error) | 	GetVaultsFunc        func() ([]onepassword.Vault, error) | ||||||
| 	GetItemFunc        func(uuid string, vaultUUID string) (*onepassword.Item, error) | 	GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) | ||||||
| 	GetItemsFunc       func(vaultUUID string) ([]onepassword.Item, error) | 	GetVaultFunc         func(uuid string) (*onepassword.Vault, error) | ||||||
| 	GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error) | 	GetItemFunc          func(uuid string, vaultUUID string) (*onepassword.Item, error) | ||||||
| 	CreateItemFunc     func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | 	GetItemsFunc         func(vaultUUID string) ([]onepassword.Item, error) | ||||||
| 	UpdateItemFunc     func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | 	GetItemsByTitleFunc  func(title string, vaultUUID string) ([]onepassword.Item, error) | ||||||
| 	DeleteItemFunc     func(item *onepassword.Item, vaultUUID string) error | 	GetItemByTitleFunc   func(title string, vaultUUID string) (*onepassword.Item, error) | ||||||
|  | 	CreateItemFunc       func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||||
|  | 	UpdateItemFunc       func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||||
|  | 	DeleteItemFunc       func(item *onepassword.Item, vaultUUID string) error | ||||||
|  | 	GetFileFunc          func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) | ||||||
|  | 	GetFileContentFunc   func(file *onepassword.File) ([]byte, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	GetGetVaultsFunc     func() ([]onepassword.Vault, error) | 	GetGetVaultsFunc       func() ([]onepassword.Vault, error) | ||||||
| 	GetGetItemFunc       func(uuid string, vaultUUID string) (*onepassword.Item, error) | 	DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) | ||||||
| 	DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error) | 	DoGetVaultFunc         func(uuid string) (*onepassword.Vault, error) | ||||||
| 	DoCreateItemFunc     func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | 	GetGetItemFunc         func(uuid string, vaultUUID string) (*onepassword.Item, error) | ||||||
| 	DoDeleteItemFunc     func(item *onepassword.Item, vaultUUID string) error | 	DoGetItemsByTitleFunc  func(title string, vaultUUID string) ([]onepassword.Item, error) | ||||||
| 	DoGetItemsFunc       func(vaultUUID string) ([]onepassword.Item, error) | 	DoGetItemByTitleFunc   func(title string, vaultUUID string) (*onepassword.Item, error) | ||||||
| 	DoUpdateItemFunc     func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | 	DoCreateItemFunc       func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||||
|  | 	DoDeleteItemFunc       func(item *onepassword.Item, vaultUUID string) error | ||||||
|  | 	DoGetItemsFunc         func(vaultUUID string) ([]onepassword.Item, error) | ||||||
|  | 	DoUpdateItemFunc       func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||||
|  | 	DoGetFileFunc          func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) | ||||||
|  | 	DoGetFileContentFunc   func(file *onepassword.File) ([]byte, error) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Do is the mock client's `Do` func | // Do is the mock client's `Do` func | ||||||
| @@ -29,6 +39,14 @@ func (m *TestClient) GetVaults() ([]onepassword.Vault, error) { | |||||||
| 	return GetGetVaultsFunc() | 	return GetGetVaultsFunc() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { | ||||||
|  | 	return DoGetVaultsByTitleFunc(title) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *TestClient) GetVault(uuid string) (*onepassword.Vault, error) { | ||||||
|  | 	return DoGetVaultFunc(uuid) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) { | func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) { | ||||||
| 	return GetGetItemFunc(uuid, vaultUUID) | 	return GetGetItemFunc(uuid, vaultUUID) | ||||||
| } | } | ||||||
| @@ -37,6 +55,10 @@ func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) { | |||||||
| 	return DoGetItemsFunc(vaultUUID) | 	return DoGetItemsFunc(vaultUUID) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (m *TestClient) GetItemsByTitle(title, vaultUUID string) ([]onepassword.Item, error) { | ||||||
|  | 	return DoGetItemsByTitleFunc(title, vaultUUID) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) { | func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) { | ||||||
| 	return DoGetItemByTitleFunc(title, vaultUUID) | 	return DoGetItemByTitleFunc(title, vaultUUID) | ||||||
| } | } | ||||||
| @@ -52,3 +74,11 @@ func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error | |||||||
| func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) { | func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) { | ||||||
| 	return DoUpdateItemFunc(item, vaultUUID) | 	return DoUpdateItemFunc(item, vaultUUID) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (m *TestClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) { | ||||||
|  | 	return DoGetFileFunc(uuid, itemUUID, vaultUUID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) { | ||||||
|  | 	return DoGetFileContentFunc(file) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,14 +4,16 @@ import ( | |||||||
| 	"regexp" | 	"regexp" | ||||||
|  |  | ||||||
| 	appsv1 "k8s.io/api/apps/v1" | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	OnepasswordPrefix  = "onepasswordoperator" | 	OnepasswordPrefix            = "operator.1password.io" | ||||||
| 	ItemPathAnnotation = OnepasswordPrefix + "/item-path" | 	ItemPathAnnotation           = OnepasswordPrefix + "/item-path" | ||||||
| 	NameAnnotation     = OnepasswordPrefix + "/item-name" | 	NameAnnotation               = OnepasswordPrefix + "/item-name" | ||||||
| 	VersionAnnotation  = OnepasswordPrefix + "/item-version" | 	VersionAnnotation            = OnepasswordPrefix + "/item-version" | ||||||
| 	RestartAnnotation  = OnepasswordPrefix + "/lastRestarted" | 	RestartAnnotation            = OnepasswordPrefix + "/last-restarted" | ||||||
|  | 	RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Regexp) (map[string]string, bool) { | func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Regexp) (map[string]string, bool) { | ||||||
| @@ -34,17 +36,25 @@ func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Re | |||||||
| func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[string]string { | func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[string]string { | ||||||
| 	filteredAnnotations := make(map[string]string) | 	filteredAnnotations := make(map[string]string) | ||||||
| 	for key, value := range annotations { | 	for key, value := range annotations { | ||||||
| 		if regex.MatchString(key) { | 		if regex.MatchString(key) && key != RestartAnnotation && key != RestartDeploymentsAnnotation { | ||||||
| 			filteredAnnotations[key] = value | 			filteredAnnotations[key] = value | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return filteredAnnotations | 	return filteredAnnotations | ||||||
| } | } | ||||||
|  |  | ||||||
| func AreAnnotationsUsingSecrets(annotations map[string]string, secrets map[string]bool) bool { | func AreAnnotationsUsingSecrets(annotations map[string]string, secrets map[string]*corev1.Secret) bool { | ||||||
| 	_, ok := secrets[annotations[NameAnnotation]] | 	_, ok := secrets[annotations[NameAnnotation]] | ||||||
| 	if ok { | 	if ok { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func AppendAnnotationUpdatedSecret(annotations map[string]string, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { | ||||||
|  | 	secret, ok := secrets[annotations[NameAnnotation]] | ||||||
|  | 	if ok { | ||||||
|  | 		updatedDeploymentSecrets[secret.Name] = secret | ||||||
|  | 	} | ||||||
|  | 	return updatedDeploymentSecrets | ||||||
|  | } | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import ( | |||||||
| 	appsv1 "k8s.io/api/apps/v1" | 	appsv1 "k8s.io/api/apps/v1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const AnnotationRegExpString = "^onepasswordoperator\\/[a-zA-Z\\.]+" | const AnnotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" | ||||||
|  |  | ||||||
| func TestFilterAnnotations(t *testing.T) { | func TestFilterAnnotations(t *testing.T) { | ||||||
| 	invalidAnnotation1 := "onepasswordconnect/vaultId" | 	invalidAnnotation1 := "onepasswordconnect/vaultId" | ||||||
|   | |||||||
							
								
								
									
										117
									
								
								pkg/onepassword/connect_setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								pkg/onepassword/connect_setup.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | package onepassword | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
|  | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	errors "k8s.io/apimachinery/pkg/api/errors" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/yaml" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var logConnectSetup = logf.Log.WithName("ConnectSetup") | ||||||
|  | var deploymentPath = "deploy/connect/deployment.yaml" | ||||||
|  | var servicePath = "deploy/connect/service.yaml" | ||||||
|  |  | ||||||
|  | func SetupConnect(kubeClient client.Client, deploymentNamespace string) error { | ||||||
|  | 	err := setupService(kubeClient, servicePath, deploymentNamespace) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { | ||||||
|  | 	existingDeployment := &appsv1.Deployment{} | ||||||
|  |  | ||||||
|  | 	// check if deployment has already been created | ||||||
|  | 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if errors.IsNotFound(err) { | ||||||
|  | 			logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") | ||||||
|  | 			return createDeployment(kubeClient, deploymentPath, deploymentNamespace) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { | ||||||
|  | 	deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = kubeClient.Create(context.Background(), deployment) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*appsv1.Deployment, error) { | ||||||
|  | 	f, err := os.Open(deploymentPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	deployment := &appsv1.Deployment{ | ||||||
|  | 		ObjectMeta: v1.ObjectMeta{ | ||||||
|  | 			Namespace: deploymentNamespace, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return deployment, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { | ||||||
|  | 	existingService := &corev1.Service{} | ||||||
|  |  | ||||||
|  | 	//check if service has already been created | ||||||
|  | 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if errors.IsNotFound(err) { | ||||||
|  | 			logConnectSetup.Info("No existing Connect service found. Creating Service") | ||||||
|  | 			return createService(kubeClient, servicePath, deploymentNamespace) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { | ||||||
|  | 	f, err := os.Open(servicePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	service := &corev1.Service{ | ||||||
|  | 		ObjectMeta: v1.ObjectMeta{ | ||||||
|  | 			Namespace: deploymentNamespace, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = kubeClient.Create(context.Background(), service) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								pkg/onepassword/connect_setup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								pkg/onepassword/connect_setup_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | package onepassword | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	"k8s.io/kubectl/pkg/scheme" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var defaultNamespacedName = types.NamespacedName{Name: "onepassword-connect", Namespace: "default"} | ||||||
|  |  | ||||||
|  | func TestServiceSetup(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	// Register operator types with the runtime scheme. | ||||||
|  | 	s := scheme.Scheme | ||||||
|  |  | ||||||
|  | 	// Objects to track in the fake client. | ||||||
|  | 	objs := []runtime.Object{} | ||||||
|  |  | ||||||
|  | 	// Create a fake client to mock API calls. | ||||||
|  | 	client := fake.NewFakeClientWithScheme(s, objs...) | ||||||
|  |  | ||||||
|  | 	err := setupService(client, "../../deploy/connect/service.yaml", defaultNamespacedName.Namespace) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Error Setting Up Connect: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check that service was created | ||||||
|  | 	service := &corev1.Service{} | ||||||
|  | 	err = client.Get(context.TODO(), defaultNamespacedName, service) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Error Setting Up Connect service: %v", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestDeploymentSetup(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	// Register operator types with the runtime scheme. | ||||||
|  | 	s := scheme.Scheme | ||||||
|  |  | ||||||
|  | 	// Objects to track in the fake client. | ||||||
|  | 	objs := []runtime.Object{} | ||||||
|  |  | ||||||
|  | 	// Create a fake client to mock API calls. | ||||||
|  | 	client := fake.NewFakeClientWithScheme(s, objs...) | ||||||
|  |  | ||||||
|  | 	err := setupDeployment(client, "../../deploy/connect/deployment.yaml", defaultNamespacedName.Namespace) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Error Setting Up Connect: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check that deployment was created | ||||||
|  | 	deployment := &appsv1.Deployment{} | ||||||
|  | 	err = client.Get(context.TODO(), defaultNamespacedName, deployment) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Error Setting Up Connect deployment: %v", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,8 +1,10 @@ | |||||||
| package onepassword | package onepassword | ||||||
|  |  | ||||||
| import corev1 "k8s.io/api/core/v1" | import ( | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | ) | ||||||
|  |  | ||||||
| func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]bool) bool { | func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool { | ||||||
| 	for i := 0; i < len(containers); i++ { | 	for i := 0; i < len(containers); i++ { | ||||||
| 		envVariables := containers[i].Env | 		envVariables := containers[i].Env | ||||||
| 		for j := 0; j < len(envVariables); j++ { | 		for j := 0; j < len(envVariables); j++ { | ||||||
| @@ -13,6 +15,39 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		envFromVariables := containers[i].EnvFrom | ||||||
|  | 		for j := 0; j < len(envFromVariables); j++ { | ||||||
|  | 			if envFromVariables[j].SecretRef != nil { | ||||||
|  | 				_, ok := secrets[envFromVariables[j].SecretRef.Name] | ||||||
|  | 				if ok { | ||||||
|  | 					return true | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { | ||||||
|  | 	for i := 0; i < len(containers); i++ { | ||||||
|  | 		envVariables := containers[i].Env | ||||||
|  | 		for j := 0; j < len(envVariables); j++ { | ||||||
|  | 			if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil { | ||||||
|  | 				secret, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name] | ||||||
|  | 				if ok { | ||||||
|  | 					updatedDeploymentSecrets[secret.Name] = secret | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		envFromVariables := containers[i].EnvFrom | ||||||
|  | 		for j := 0; j < len(envFromVariables); j++ { | ||||||
|  | 			if envFromVariables[j].SecretRef != nil { | ||||||
|  | 				secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name] | ||||||
|  | 				if ok { | ||||||
|  | 					updatedDeploymentSecrets[secret.Name] = secret | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return updatedDeploymentSecrets | ||||||
|  | } | ||||||
|   | |||||||
| @@ -2,12 +2,15 @@ package onepassword | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestAreContainersUsingSecrets(t *testing.T) { | func TestAreContainersUsingSecretsFromEnv(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]bool{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": true, | 		"onepassword-database-secret": &corev1.Secret{}, | ||||||
| 		"onepassword-api-key":         true, | 		"onepassword-api-key":         &corev1.Secret{}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containerSecretNames := []string{ | 	containerSecretNames := []string{ | ||||||
| @@ -16,7 +19,26 @@ func TestAreContainersUsingSecrets(t *testing.T) { | |||||||
| 		"some_other_key", | 		"some_other_key", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containers := generateContainers(containerSecretNames) | 	containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) | ||||||
|  |  | ||||||
|  | 	if !AreContainersUsingSecrets(containers, secretNamesToSearch) { | ||||||
|  | 		t.Errorf("Expected that containers were using secrets but they were not detected.") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAreContainersUsingSecretsFromEnvFrom(t *testing.T) { | ||||||
|  | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
|  | 		"onepassword-database-secret": {}, | ||||||
|  | 		"onepassword-api-key":         {}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	containerSecretNames := []string{ | ||||||
|  | 		"onepassword-database-secret", | ||||||
|  | 		"onepassword-api-key", | ||||||
|  | 		"some_other_key", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) | ||||||
|  |  | ||||||
| 	if !AreContainersUsingSecrets(containers, secretNamesToSearch) { | 	if !AreContainersUsingSecrets(containers, secretNamesToSearch) { | ||||||
| 		t.Errorf("Expected that containers were using secrets but they were not detected.") | 		t.Errorf("Expected that containers were using secrets but they were not detected.") | ||||||
| @@ -24,18 +46,40 @@ func TestAreContainersUsingSecrets(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func TestAreContainersNotUsingSecrets(t *testing.T) { | func TestAreContainersNotUsingSecrets(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]bool{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": true, | 		"onepassword-database-secret": {}, | ||||||
| 		"onepassword-api-key":         true, | 		"onepassword-api-key":         {}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containerSecretNames := []string{ | 	containerSecretNames := []string{ | ||||||
| 		"some_other_key", | 		"some_other_key", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containers := generateContainers(containerSecretNames) | 	containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) | ||||||
|  |  | ||||||
| 	if AreContainersUsingSecrets(containers, secretNamesToSearch) { | 	if AreContainersUsingSecrets(containers, secretNamesToSearch) { | ||||||
| 		t.Errorf("Expected that containers were not using secrets but they were detected.") | 		t.Errorf("Expected that containers were not using secrets but they were detected.") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestAppendUpdatedContainerSecretsParsesEnvFromEnv(t *testing.T) { | ||||||
|  | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
|  | 		"onepassword-database-secret": {}, | ||||||
|  | 		"onepassword-api-key":         {ObjectMeta: metav1.ObjectMeta{Name: "onepassword-api-key"}}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	containerSecretNames := []string{ | ||||||
|  | 		"onepassword-api-key", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) | ||||||
|  |  | ||||||
|  | 	updatedDeploymentSecrets := map[string]*corev1.Secret{} | ||||||
|  | 	updatedDeploymentSecrets = AppendUpdatedContainerSecrets(containers, secretNamesToSearch, updatedDeploymentSecrets) | ||||||
|  |  | ||||||
|  | 	secretKeyName := "onepassword-api-key" | ||||||
|  |  | ||||||
|  | 	if updatedDeploymentSecrets[secretKeyName] != secretNamesToSearch[secretKeyName] { | ||||||
|  | 		t.Errorf("Expected that updated Secret from envfrom is found.") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,10 +1,26 @@ | |||||||
| package onepassword | package onepassword | ||||||
|  |  | ||||||
| import appsv1 "k8s.io/api/apps/v1" | import ( | ||||||
|  | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | ) | ||||||
|  |  | ||||||
| func IsDeploymentUsingSecrets(deployment *appsv1.Deployment, secrets map[string]bool) bool { | func IsDeploymentUsingSecrets(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) bool { | ||||||
| 	volumes := deployment.Spec.Template.Spec.Volumes | 	volumes := deployment.Spec.Template.Spec.Volumes | ||||||
| 	containers := deployment.Spec.Template.Spec.Containers | 	containers := deployment.Spec.Template.Spec.Containers | ||||||
| 	containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) | 	containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) | ||||||
| 	return AreAnnotationsUsingSecrets(deployment.Annotations, secrets) || AreContainersUsingSecrets(containers, secrets) || AreVolumesUsingSecrets(volumes, secrets) | 	return AreAnnotationsUsingSecrets(deployment.Annotations, secrets) || AreContainersUsingSecrets(containers, secrets) || AreVolumesUsingSecrets(volumes, secrets) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func GetUpdatedSecretsForDeployment(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) map[string]*corev1.Secret { | ||||||
|  | 	volumes := deployment.Spec.Template.Spec.Volumes | ||||||
|  | 	containers := deployment.Spec.Template.Spec.Containers | ||||||
|  | 	containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) | ||||||
|  |  | ||||||
|  | 	updatedSecretsForDeployment := map[string]*corev1.Secret{} | ||||||
|  | 	AppendAnnotationUpdatedSecret(deployment.Annotations, secrets, updatedSecretsForDeployment) | ||||||
|  | 	AppendUpdatedContainerSecrets(containers, secrets, updatedSecretsForDeployment) | ||||||
|  | 	AppendUpdatedVolumeSecrets(volumes, secrets, updatedSecretsForDeployment) | ||||||
|  |  | ||||||
|  | 	return updatedSecretsForDeployment | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,12 +4,13 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	appsv1 "k8s.io/api/apps/v1" | 	appsv1 "k8s.io/api/apps/v1" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) { | func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]bool{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": true, | 		"onepassword-database-secret": &corev1.Secret{}, | ||||||
| 		"onepassword-api-key":         true, | 		"onepassword-api-key":         &corev1.Secret{}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	volumeSecretNames := []string{ | 	volumeSecretNames := []string{ | ||||||
| @@ -26,9 +27,9 @@ func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { | func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]bool{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": true, | 		"onepassword-database-secret": &corev1.Secret{}, | ||||||
| 		"onepassword-api-key":         true, | 		"onepassword-api-key":         &corev1.Secret{}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containerSecretNames := []string{ | 	containerSecretNames := []string{ | ||||||
| @@ -38,16 +39,16 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	deployment := &appsv1.Deployment{} | 	deployment := &appsv1.Deployment{} | ||||||
| 	deployment.Spec.Template.Spec.Containers = generateContainers(containerSecretNames) | 	deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames) | ||||||
| 	if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { | 	if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { | ||||||
| 		t.Errorf("Expected that deployment was using secrets but they were not detected.") | 		t.Errorf("Expected that deployment was using secrets but they were not detected.") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestIsDeploymentNotUSingSecrets(t *testing.T) { | func TestIsDeploymentNotUSingSecrets(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]bool{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": true, | 		"onepassword-database-secret": &corev1.Secret{}, | ||||||
| 		"onepassword-api-key":         true, | 		"onepassword-api-key":         &corev1.Secret{}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	deployment := &appsv1.Deployment{} | 	deployment := &appsv1.Deployment{} | ||||||
|   | |||||||
| @@ -6,24 +6,95 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/connect" | 	"github.com/1Password/connect-sdk-go/connect" | ||||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | 	"github.com/1Password/connect-sdk-go/onepassword" | ||||||
|  | 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var logger = logf.Log.WithName("retrieve_item") | ||||||
|  |  | ||||||
| func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) { | func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) { | ||||||
| 	vaultId, itemId, err := ParseVaultIdAndItemIdFromPath(path) | 	vaultValue, itemValue, err := ParseVaultAndItemFromPath(path) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | 	vaultId, err := getVaultId(opConnectClient, vaultValue) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	itemId, err := getItemId(opConnectClient, itemValue, vaultId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	item, err := opConnectClient.GetItem(itemId, vaultId) | 	item, err := opConnectClient.GetItem(itemId, vaultId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	for _, file := range item.Files { | ||||||
|  | 		_, err := opConnectClient.GetFileContent(file) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return item, nil | 	return item, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) { | func ParseVaultAndItemFromPath(path string) (string, string, error) { | ||||||
| 	splitPath := strings.Split(path, "/") | 	splitPath := strings.Split(path, "/") | ||||||
| 	if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { | 	if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { | ||||||
| 		return splitPath[1], splitPath[3], nil | 		return splitPath[1], splitPath[3], nil | ||||||
| 	} | 	} | ||||||
| 	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) | 	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) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func getVaultId(client connect.Client, vaultIdentifier string) (string, error) { | ||||||
|  | 	if !IsValidClientUUID(vaultIdentifier) { | ||||||
|  | 		vaults, err := client.GetVaultsByTitle(vaultIdentifier) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(vaults) == 0 { | ||||||
|  | 			return "", fmt.Errorf("No vaults found with identifier %q", vaultIdentifier) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		oldestVault := vaults[0] | ||||||
|  | 		if len(vaults) > 1 { | ||||||
|  | 			for _, returnedVault := range vaults { | ||||||
|  | 				if returnedVault.CreatedAt.Before(oldestVault.CreatedAt) { | ||||||
|  | 					oldestVault = returnedVault | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			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)) | ||||||
|  | 		} | ||||||
|  | 		vaultIdentifier = oldestVault.ID | ||||||
|  | 	} | ||||||
|  | 	return vaultIdentifier, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getItemId(client connect.Client, itemIdentifier string, vaultId string) (string, error) { | ||||||
|  | 	if !IsValidClientUUID(itemIdentifier) { | ||||||
|  | 		items, err := client.GetItemsByTitle(itemIdentifier, vaultId) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(items) == 0 { | ||||||
|  | 			return "", fmt.Errorf("No items found with identifier %q", itemIdentifier) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		oldestItem := items[0] | ||||||
|  | 		if len(items) > 1 { | ||||||
|  | 			for _, returnedItem := range items { | ||||||
|  | 				if returnedItem.CreatedAt.Before(oldestItem.CreatedAt) { | ||||||
|  | 					oldestItem = returnedItem | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			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)) | ||||||
|  | 		} | ||||||
|  | 		itemIdentifier = oldestItem.ID | ||||||
|  | 	} | ||||||
|  | 	return itemIdentifier, nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -17,8 +17,7 @@ func generateVolumes(names []string) []corev1.Volume { | |||||||
| 	} | 	} | ||||||
| 	return volumes | 	return volumes | ||||||
| } | } | ||||||
|  | func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container { | ||||||
| func generateContainers(names []string) []corev1.Container { |  | ||||||
| 	containers := []corev1.Container{} | 	containers := []corev1.Container{} | ||||||
| 	for i := 0; i < len(names); i++ { | 	for i := 0; i < len(names); i++ { | ||||||
| 		container := corev1.Container{ | 		container := corev1.Container{ | ||||||
| @@ -40,3 +39,16 @@ func generateContainers(names []string) []corev1.Container { | |||||||
| 	} | 	} | ||||||
| 	return containers | 	return containers | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func generateContainersWithSecretRefsFromEnvFrom(names []string) []corev1.Container { | ||||||
|  | 	containers := []corev1.Container{} | ||||||
|  | 	for i := 0; i < len(names); i++ { | ||||||
|  | 		container := corev1.Container{ | ||||||
|  | 			EnvFrom: []corev1.EnvFromSource{ | ||||||
|  | 				{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: names[i]}}}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 		containers = append(containers, container) | ||||||
|  | 	} | ||||||
|  | 	return containers | ||||||
|  | } | ||||||
|   | |||||||
| @@ -5,9 +5,13 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	v1 "github.com/1Password/onepassword-operator/api/v1" | ||||||
|  |  | ||||||
| 	kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" | 	kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" | ||||||
|  | 	"github.com/1Password/onepassword-operator/pkg/utils" | ||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/connect" | 	"github.com/1Password/connect-sdk-go/connect" | ||||||
|  | 	"github.com/1Password/connect-sdk-go/onepassword" | ||||||
| 	appsv1 "k8s.io/api/apps/v1" | 	appsv1 "k8s.io/api/apps/v1" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| @@ -15,19 +19,22 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| const envHostVariable = "OP_HOST" | const envHostVariable = "OP_HOST" | ||||||
|  | const lockTag = "operator.1password.io:ignore-secret" | ||||||
|  |  | ||||||
| var log = logf.Log.WithName("update_op_kubernetes_secrets_task") | var log = logf.Log.WithName("update_op_kubernetes_secrets_task") | ||||||
|  |  | ||||||
| func NewManager(kubernetesClient client.Client, opConnectClient connect.Client) *SecretUpdateHandler { | func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler { | ||||||
| 	return &SecretUpdateHandler{ | 	return &SecretUpdateHandler{ | ||||||
| 		client:          kubernetesClient, | 		client:                             kubernetesClient, | ||||||
| 		opConnectClient: opConnectClient, | 		opConnectClient:                    opConnectClient, | ||||||
|  | 		shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| type SecretUpdateHandler struct { | type SecretUpdateHandler struct { | ||||||
| 	client          client.Client | 	client                             client.Client | ||||||
| 	opConnectClient connect.Client | 	opConnectClient                    connect.Client | ||||||
|  | 	shouldAutoRestartDeploymentsGlobal bool | ||||||
| } | } | ||||||
|  |  | ||||||
| func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error { | func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error { | ||||||
| @@ -39,7 +46,7 @@ func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error { | |||||||
| 	return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets) | 	return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]bool) error { | func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret) error { | ||||||
| 	// No secrets to update. Exit | 	// No secrets to update. Exit | ||||||
| 	if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil { | 	if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil { | ||||||
| 		return nil | 		return nil | ||||||
| @@ -52,22 +59,38 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if len(deployments.Items) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for i := 0; i < len(deployments.Items); i++ { | 	for i := 0; i < len(deployments.Items); i++ { | ||||||
| 		deployment := &deployments.Items[i] | 		deployment := &deployments.Items[i] | ||||||
| 		updatedSecrets := updatedSecretsByNamespace[deployment.Namespace] | 		updatedSecrets := updatedSecretsByNamespace[deployment.Namespace] | ||||||
| 		secretName := deployment.Annotations[NameAnnotation] |  | ||||||
| 		log.Info(fmt.Sprintf("Looking at secret %v for deployment %v", secretName, deployment.Name)) | 		updatedDeploymentSecrets := GetUpdatedSecretsForDeployment(deployment, updatedSecrets) | ||||||
| 		if isUpdatedSecret(secretName, updatedSecrets) || IsDeploymentUsingSecrets(deployment, updatedSecrets) { | 		if len(updatedDeploymentSecrets) == 0 { | ||||||
| 			h.restartDeployment(deployment) | 			continue | ||||||
| 		} else { |  | ||||||
| 			log.Info(fmt.Sprintf("Deployment '%v' is up to date", deployment.GetName())) |  | ||||||
| 		} | 		} | ||||||
|  | 		for _, secret := range updatedDeploymentSecrets { | ||||||
|  | 			if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) { | ||||||
|  | 				h.restartDeployment(deployment) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		log.Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace)) | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) { | func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) { | ||||||
| 	log.Info(fmt.Sprintf("Deployment '%v' references an updated secret. Restarting", deployment.GetName())) | 	log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace)) | ||||||
| 	deployment.Spec.Template.Annotations = map[string]string{ | 	deployment.Spec.Template.Annotations = map[string]string{ | ||||||
| 		RestartAnnotation: time.Now().String(), | 		RestartAnnotation: time.Now().String(), | ||||||
| 	} | 	} | ||||||
| @@ -77,7 +100,7 @@ func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]bool, error) { | func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*corev1.Secret, error) { | ||||||
| 	secrets := &corev1.SecretList{} | 	secrets := &corev1.SecretList{} | ||||||
| 	err := h.client.List(context.Background(), secrets) | 	err := h.client.List(context.Background(), secrets) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -85,7 +108,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]b | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	updatedSecrets := map[string]map[string]bool{} | 	updatedSecrets := map[string]map[string]*corev1.Secret{} | ||||||
| 	for i := 0; i < len(secrets.Items); i++ { | 	for i := 0; i < len(secrets.Items); i++ { | ||||||
| 		secret := secrets.Items[i] | 		secret := secrets.Items[i] | ||||||
|  |  | ||||||
| @@ -95,30 +118,131 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]b | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation]) | 		OnePasswordItemPath := h.getPathFromOnePasswordItem(secret) | ||||||
|  |  | ||||||
|  | 		item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("Failed to retrieve item: %v", err) | 			log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name) | ||||||
|  | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		itemVersion := fmt.Sprint(item.Version) | 		itemVersion := fmt.Sprint(item.Version) | ||||||
| 		if currentVersion != itemVersion { | 		itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID) | ||||||
|  |  | ||||||
|  | 		if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString { | ||||||
|  | 			if isItemLockedForForcedRestarts(item) { | ||||||
|  | 				log.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())) | ||||||
|  | 				secret.Annotations[VersionAnnotation] = itemVersion | ||||||
|  | 				secret.Annotations[ItemPathAnnotation] = itemPathString | ||||||
|  | 				h.client.Update(context.Background(), &secret) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
| 			log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) | 			log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) | ||||||
| 			secret.Annotations[VersionAnnotation] = itemVersion | 			secret.Annotations[VersionAnnotation] = itemVersion | ||||||
| 			updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item) | 			secret.Annotations[ItemPathAnnotation] = itemPathString | ||||||
|  | 			updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, string(secret.Type), *item, nil) | ||||||
|  | 			log.Info(fmt.Sprintf("New secret path: %v and version: %v", updatedSecret.Annotations[ItemPathAnnotation], updatedSecret.Annotations[VersionAnnotation])) | ||||||
| 			h.client.Update(context.Background(), updatedSecret) | 			h.client.Update(context.Background(), updatedSecret) | ||||||
| 			if updatedSecrets[secret.Namespace] == nil { | 			if updatedSecrets[secret.Namespace] == nil { | ||||||
| 				updatedSecrets[secret.Namespace] = make(map[string]bool) | 				updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret) | ||||||
| 			} | 			} | ||||||
| 			updatedSecrets[secret.Namespace][secret.Name] = true | 			updatedSecrets[secret.Namespace][secret.Name] = &secret | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return updatedSecrets, nil | 	return updatedSecrets, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func isUpdatedSecret(secretName string, updatedSecrets map[string]bool) bool { | func isItemLockedForForcedRestarts(item *onepassword.Item) bool { | ||||||
|  | 	tags := item.Tags | ||||||
|  | 	for i := 0; i < len(tags); i++ { | ||||||
|  | 		if tags[i] == lockTag { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isUpdatedSecret(secretName string, updatedSecrets map[string]*corev1.Secret) bool { | ||||||
| 	_, ok := updatedSecrets[secretName] | 	_, ok := updatedSecrets[secretName] | ||||||
| 	if ok { | 	if ok { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string]bool, error) { | ||||||
|  | 	namespaces := &corev1.NamespaceList{} | ||||||
|  | 	err := h.client.List(context.Background(), namespaces) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error(err, "Failed to list kubernetes namespaces") | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	namespacesMap := map[string]bool{} | ||||||
|  |  | ||||||
|  | 	for _, namespace := range namespaces.Items { | ||||||
|  | 		namespacesMap[namespace.Name] = h.isNamespaceSetToAutoRestart(&namespace) | ||||||
|  | 	} | ||||||
|  | 	return namespacesMap, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string { | ||||||
|  | 	onePasswordItem := &v1.OnePasswordItem{} | ||||||
|  |  | ||||||
|  | 	// Search for our original OnePasswordItem if it exists | ||||||
|  | 	err := h.client.Get(context.TODO(), client.ObjectKey{ | ||||||
|  | 		Namespace: secret.Namespace, | ||||||
|  | 		Name:      secret.Name}, onePasswordItem) | ||||||
|  |  | ||||||
|  | 	if err == nil { | ||||||
|  | 		return onePasswordItem.Spec.ItemPath | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If we can't find the OnePassword Item we'll just return the annotation from the secret item. | ||||||
|  | 	return secret.Annotations[ItemPathAnnotation] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { | ||||||
|  | 	restartDeployment := secret.Annotations[RestartDeploymentsAnnotation] | ||||||
|  | 	//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace | ||||||
|  | 	if restartDeployment == "" { | ||||||
|  | 		return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	restartDeploymentBool, err := utils.StringToBool(restartDeployment) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secret.Name) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return restartDeploymentBool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isDeploymentSetForAutoRestart(deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { | ||||||
|  | 	restartDeployment := deployment.Annotations[RestartDeploymentsAnnotation] | ||||||
|  | 	//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace | ||||||
|  | 	if restartDeployment == "" { | ||||||
|  | 		return setForAutoRestartByNamespace[deployment.Namespace] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	restartDeploymentBool, err := utils.StringToBool(restartDeployment) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error(err, "Error parsing %v annotation on Deployment %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, deployment.Name) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return restartDeploymentBool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *SecretUpdateHandler) isNamespaceSetToAutoRestart(namespace *corev1.Namespace) bool { | ||||||
|  | 	restartDeployment := namespace.Annotations[RestartDeploymentsAnnotation] | ||||||
|  | 	//If annotation for auto restarts for deployment is not set. Check environment variable set on the operator | ||||||
|  | 	if restartDeployment == "" { | ||||||
|  | 		return h.shouldAutoRestartDeploymentsGlobal | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	restartDeploymentBool, err := utils.StringToBool(restartDeployment) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error(err, "Error parsing %v annotation on Namespace %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, namespace.Name) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return restartDeploymentBool | ||||||
|  | } | ||||||
|   | |||||||
| @@ -34,14 +34,16 @@ const ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type testUpdateSecretTask struct { | type testUpdateSecretTask struct { | ||||||
| 	testName             string | 	testName                 string | ||||||
| 	existingDeployment   *appsv1.Deployment | 	existingDeployment       *appsv1.Deployment | ||||||
| 	existingSecret       *corev1.Secret | 	existingNamespace        *corev1.Namespace | ||||||
| 	expectedError        error | 	existingSecret           *corev1.Secret | ||||||
| 	expectedResultSecret *corev1.Secret | 	expectedError            error | ||||||
| 	expectedEvents       []string | 	expectedResultSecret     *corev1.Secret | ||||||
| 	opItem               map[string]string | 	expectedEvents           []string | ||||||
| 	expectedRestart      bool | 	opItem                   map[string]string | ||||||
|  | 	expectedRestart          bool | ||||||
|  | 	globalAutoRestartEnabled bool | ||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| @@ -52,9 +54,16 @@ var ( | |||||||
| 	itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId) | 	itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId) | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var defaultNamespace = &corev1.Namespace{ | ||||||
|  | 	ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 		Name: namespace, | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  |  | ||||||
| var tests = []testUpdateSecretTask{ | var tests = []testUpdateSecretTask{ | ||||||
| 	{ | 	{ | ||||||
| 		testName: "Test unrelated deployment is not restarted with an updated secret", | 		testName:          "Test unrelated deployment is not restarted with an updated secret", | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
| 		existingDeployment: &appsv1.Deployment{ | 		existingDeployment: &appsv1.Deployment{ | ||||||
| 			TypeMeta: metav1.TypeMeta{ | 			TypeMeta: metav1.TypeMeta{ | ||||||
| 				Kind:       deploymentKind, | 				Kind:       deploymentKind, | ||||||
| @@ -96,10 +105,12 @@ var tests = []testUpdateSecretTask{ | |||||||
| 			userKey: username, | 			userKey: username, | ||||||
| 			passKey: password, | 			passKey: password, | ||||||
| 		}, | 		}, | ||||||
| 		expectedRestart: false, | 		expectedRestart:          false, | ||||||
|  | 		globalAutoRestartEnabled: true, | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		testName: "OP item has new version. Secret needs update. Deployment is restarted based on containers", | 		testName:          "OP item has new version. Secret needs update. Deployment is restarted based on containers", | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
| 		existingDeployment: &appsv1.Deployment{ | 		existingDeployment: &appsv1.Deployment{ | ||||||
| 			TypeMeta: metav1.TypeMeta{ | 			TypeMeta: metav1.TypeMeta{ | ||||||
| 				Kind:       deploymentKind, | 				Kind:       deploymentKind, | ||||||
| @@ -160,10 +171,12 @@ var tests = []testUpdateSecretTask{ | |||||||
| 			userKey: username, | 			userKey: username, | ||||||
| 			passKey: password, | 			passKey: password, | ||||||
| 		}, | 		}, | ||||||
| 		expectedRestart: true, | 		expectedRestart:          true, | ||||||
|  | 		globalAutoRestartEnabled: true, | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		testName: "OP item has new version. Secret needs update. Deployment is restarted based on annotation", | 		testName:          "OP item has new version. Secret needs update. Deployment is restarted based on annotation", | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
| 		existingDeployment: &appsv1.Deployment{ | 		existingDeployment: &appsv1.Deployment{ | ||||||
| 			TypeMeta: metav1.TypeMeta{ | 			TypeMeta: metav1.TypeMeta{ | ||||||
| 				Kind:       deploymentKind, | 				Kind:       deploymentKind, | ||||||
| @@ -205,10 +218,12 @@ var tests = []testUpdateSecretTask{ | |||||||
| 			userKey: username, | 			userKey: username, | ||||||
| 			passKey: password, | 			passKey: password, | ||||||
| 		}, | 		}, | ||||||
| 		expectedRestart: true, | 		expectedRestart:          true, | ||||||
|  | 		globalAutoRestartEnabled: true, | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		testName: "OP item has new version. Secret needs update. Deployment is restarted based on volume", | 		testName:          "OP item has new version. Secret needs update. Deployment is restarted based on volume", | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
| 		existingDeployment: &appsv1.Deployment{ | 		existingDeployment: &appsv1.Deployment{ | ||||||
| 			TypeMeta: metav1.TypeMeta{ | 			TypeMeta: metav1.TypeMeta{ | ||||||
| 				Kind:       deploymentKind, | 				Kind:       deploymentKind, | ||||||
| @@ -262,10 +277,12 @@ var tests = []testUpdateSecretTask{ | |||||||
| 			userKey: username, | 			userKey: username, | ||||||
| 			passKey: password, | 			passKey: password, | ||||||
| 		}, | 		}, | ||||||
| 		expectedRestart: true, | 		expectedRestart:          true, | ||||||
|  | 		globalAutoRestartEnabled: true, | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		testName: "No secrets need update. No deployment is restarted", | 		testName:          "No secrets need update. No deployment is restarted", | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
| 		existingDeployment: &appsv1.Deployment{ | 		existingDeployment: &appsv1.Deployment{ | ||||||
| 			TypeMeta: metav1.TypeMeta{ | 			TypeMeta: metav1.TypeMeta{ | ||||||
| 				Kind:       deploymentKind, | 				Kind:       deploymentKind, | ||||||
| @@ -307,11 +324,440 @@ var tests = []testUpdateSecretTask{ | |||||||
| 			userKey: username, | 			userKey: username, | ||||||
| 			passKey: password, | 			passKey: password, | ||||||
| 		}, | 		}, | ||||||
| 		expectedRestart: false, | 		expectedRestart:          false, | ||||||
|  | 		globalAutoRestartEnabled: true, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName: `Deployment is not restarted when no auto restart is set to true for all | ||||||
|  | 		deployments and is not overwritten by by a namespace or deployment annotation`, | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
|  | 		existingDeployment: &appsv1.Deployment{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       deploymentKind, | ||||||
|  | 				APIVersion: deploymentAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 			}, | ||||||
|  | 			Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Containers: []corev1.Container{ | ||||||
|  | 							{ | ||||||
|  | 								Env: []corev1.EnvVar{ | ||||||
|  | 									{ | ||||||
|  | 										Name: name, | ||||||
|  | 										ValueFrom: &corev1.EnvVarSource{ | ||||||
|  | 											SecretKeyRef: &corev1.SecretKeySelector{ | ||||||
|  | 												LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 													Name: name, | ||||||
|  | 												}, | ||||||
|  | 												Key: passKey, | ||||||
|  | 											}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:  "old version", | ||||||
|  | 					ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		expectedError: nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:  fmt.Sprint(itemVersion), | ||||||
|  | 					ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
|  | 		}, | ||||||
|  | 		expectedRestart:          false, | ||||||
|  | 		globalAutoRestartEnabled: false, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName:          `Secret autostart true value takes precedence over false deployment value`, | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
|  | 		existingDeployment: &appsv1.Deployment{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       deploymentKind, | ||||||
|  | 				APIVersion: deploymentAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					RestartDeploymentsAnnotation: "false", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Containers: []corev1.Container{ | ||||||
|  | 							{ | ||||||
|  | 								Env: []corev1.EnvVar{ | ||||||
|  | 									{ | ||||||
|  | 										Name: name, | ||||||
|  | 										ValueFrom: &corev1.EnvVarSource{ | ||||||
|  | 											SecretKeyRef: &corev1.SecretKeySelector{ | ||||||
|  | 												LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 													Name: name, | ||||||
|  | 												}, | ||||||
|  | 												Key: passKey, | ||||||
|  | 											}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:            "old version", | ||||||
|  | 					ItemPathAnnotation:           itemPath, | ||||||
|  | 					RestartDeploymentsAnnotation: "true", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		expectedError: nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:            fmt.Sprint(itemVersion), | ||||||
|  | 					ItemPathAnnotation:           itemPath, | ||||||
|  | 					RestartDeploymentsAnnotation: "true", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
|  | 		}, | ||||||
|  | 		expectedRestart:          true, | ||||||
|  | 		globalAutoRestartEnabled: false, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName:          `Secret autostart true value takes precedence over false deployment value`, | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
|  | 		existingDeployment: &appsv1.Deployment{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       deploymentKind, | ||||||
|  | 				APIVersion: deploymentAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					RestartDeploymentsAnnotation: "true", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Containers: []corev1.Container{ | ||||||
|  | 							{ | ||||||
|  | 								Env: []corev1.EnvVar{ | ||||||
|  | 									{ | ||||||
|  | 										Name: name, | ||||||
|  | 										ValueFrom: &corev1.EnvVarSource{ | ||||||
|  | 											SecretKeyRef: &corev1.SecretKeySelector{ | ||||||
|  | 												LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 													Name: name, | ||||||
|  | 												}, | ||||||
|  | 												Key: passKey, | ||||||
|  | 											}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:            "old version", | ||||||
|  | 					ItemPathAnnotation:           itemPath, | ||||||
|  | 					RestartDeploymentsAnnotation: "false", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		expectedError: nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:            fmt.Sprint(itemVersion), | ||||||
|  | 					ItemPathAnnotation:           itemPath, | ||||||
|  | 					RestartDeploymentsAnnotation: "false", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
|  | 		}, | ||||||
|  | 		expectedRestart:          false, | ||||||
|  | 		globalAutoRestartEnabled: true, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName:          `Deployment autostart true value takes precedence over false global auto restart value`, | ||||||
|  | 		existingNamespace: defaultNamespace, | ||||||
|  | 		existingDeployment: &appsv1.Deployment{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       deploymentKind, | ||||||
|  | 				APIVersion: deploymentAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					RestartDeploymentsAnnotation: "true", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Containers: []corev1.Container{ | ||||||
|  | 							{ | ||||||
|  | 								Env: []corev1.EnvVar{ | ||||||
|  | 									{ | ||||||
|  | 										Name: name, | ||||||
|  | 										ValueFrom: &corev1.EnvVarSource{ | ||||||
|  | 											SecretKeyRef: &corev1.SecretKeySelector{ | ||||||
|  | 												LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 													Name: name, | ||||||
|  | 												}, | ||||||
|  | 												Key: passKey, | ||||||
|  | 											}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:  "old version", | ||||||
|  | 					ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		expectedError: nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:  fmt.Sprint(itemVersion), | ||||||
|  | 					ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
|  | 		}, | ||||||
|  | 		expectedRestart:          true, | ||||||
|  | 		globalAutoRestartEnabled: false, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName: `Deployment autostart false value takes precedence over false global auto restart value, | ||||||
|  | 		 and true namespace value.`, | ||||||
|  | 		existingNamespace: &corev1.Namespace{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					RestartDeploymentsAnnotation: "true", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingDeployment: &appsv1.Deployment{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       deploymentKind, | ||||||
|  | 				APIVersion: deploymentAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					RestartDeploymentsAnnotation: "false", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Containers: []corev1.Container{ | ||||||
|  | 							{ | ||||||
|  | 								Env: []corev1.EnvVar{ | ||||||
|  | 									{ | ||||||
|  | 										Name: name, | ||||||
|  | 										ValueFrom: &corev1.EnvVarSource{ | ||||||
|  | 											SecretKeyRef: &corev1.SecretKeySelector{ | ||||||
|  | 												LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 													Name: name, | ||||||
|  | 												}, | ||||||
|  | 												Key: passKey, | ||||||
|  | 											}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:  "old version", | ||||||
|  | 					ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		expectedError: nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:  fmt.Sprint(itemVersion), | ||||||
|  | 					ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
|  | 		}, | ||||||
|  | 		expectedRestart:          false, | ||||||
|  | 		globalAutoRestartEnabled: false, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName: `Namespace autostart true value takes precedence over false global auto restart value`, | ||||||
|  | 		existingNamespace: &corev1.Namespace{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					RestartDeploymentsAnnotation: "true", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingDeployment: &appsv1.Deployment{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       deploymentKind, | ||||||
|  | 				APIVersion: deploymentAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 			}, | ||||||
|  | 			Spec: appsv1.DeploymentSpec{ | ||||||
|  | 				Template: corev1.PodTemplateSpec{ | ||||||
|  | 					Spec: corev1.PodSpec{ | ||||||
|  | 						Containers: []corev1.Container{ | ||||||
|  | 							{ | ||||||
|  | 								Env: []corev1.EnvVar{ | ||||||
|  | 									{ | ||||||
|  | 										Name: name, | ||||||
|  | 										ValueFrom: &corev1.EnvVarSource{ | ||||||
|  | 											SecretKeyRef: &corev1.SecretKeySelector{ | ||||||
|  | 												LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 													Name: name, | ||||||
|  | 												}, | ||||||
|  | 												Key: passKey, | ||||||
|  | 											}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:  "old version", | ||||||
|  | 					ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		expectedError: nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					VersionAnnotation:  fmt.Sprint(itemVersion), | ||||||
|  | 					ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
|  | 		}, | ||||||
|  | 		expectedRestart:          true, | ||||||
|  | 		globalAutoRestartEnabled: false, | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestReconcileDepoyment(t *testing.T) { | func TestUpdateSecretHandler(t *testing.T) { | ||||||
| 	for _, testData := range tests { | 	for _, testData := range tests { | ||||||
| 		t.Run(testData.testName, func(t *testing.T) { | 		t.Run(testData.testName, func(t *testing.T) { | ||||||
|  |  | ||||||
| @@ -322,6 +768,7 @@ func TestReconcileDepoyment(t *testing.T) { | |||||||
| 			// Objects to track in the fake client. | 			// Objects to track in the fake client. | ||||||
| 			objs := []runtime.Object{ | 			objs := []runtime.Object{ | ||||||
| 				testData.existingDeployment, | 				testData.existingDeployment, | ||||||
|  | 				testData.existingNamespace, | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if testData.existingSecret != nil { | 			if testData.existingSecret != nil { | ||||||
| @@ -342,8 +789,9 @@ func TestReconcileDepoyment(t *testing.T) { | |||||||
| 				return &item, nil | 				return &item, nil | ||||||
| 			} | 			} | ||||||
| 			h := &SecretUpdateHandler{ | 			h := &SecretUpdateHandler{ | ||||||
| 				client:          cl, | 				client:                             cl, | ||||||
| 				opConnectClient: opConnectClient, | 				opConnectClient:                    opConnectClient, | ||||||
|  | 				shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled, | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			err := h.UpdateKubernetesSecretsTask() | 			err := h.UpdateKubernetesSecretsTask() | ||||||
| @@ -377,9 +825,9 @@ func TestReconcileDepoyment(t *testing.T) { | |||||||
|  |  | ||||||
| 			_, ok := deployment.Spec.Template.Annotations[RestartAnnotation] | 			_, ok := deployment.Spec.Template.Annotations[RestartAnnotation] | ||||||
| 			if ok { | 			if ok { | ||||||
| 				assert.True(t, testData.expectedRestart) | 				assert.True(t, testData.expectedRestart, "Expected deployment to restart but it did not") | ||||||
| 			} else { | 			} else { | ||||||
| 				assert.False(t, testData.expectedRestart) | 				assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.") | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| @@ -388,12 +836,12 @@ func TestReconcileDepoyment(t *testing.T) { | |||||||
| func TestIsUpdatedSecret(t *testing.T) { | func TestIsUpdatedSecret(t *testing.T) { | ||||||
|  |  | ||||||
| 	secretName := "test-secret" | 	secretName := "test-secret" | ||||||
| 	updatedSecrets := map[string]bool{ | 	updatedSecrets := map[string]*corev1.Secret{ | ||||||
| 		"some_secret": true, | 		"some_secret": &corev1.Secret{}, | ||||||
| 	} | 	} | ||||||
| 	assert.False(t, isUpdatedSecret(secretName, updatedSecrets)) | 	assert.False(t, isUpdatedSecret(secretName, updatedSecrets)) | ||||||
|  |  | ||||||
| 	updatedSecrets[secretName] = true | 	updatedSecrets[secretName] = &corev1.Secret{} | ||||||
| 	assert.True(t, isUpdatedSecret(secretName, updatedSecrets)) | 	assert.True(t, isUpdatedSecret(secretName, updatedSecrets)) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								pkg/onepassword/uuid.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								pkg/onepassword/uuid.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | package onepassword | ||||||
|  |  | ||||||
|  | // UUIDLength defines the required length of UUIDs | ||||||
|  | const UUIDLength = 26 | ||||||
|  |  | ||||||
|  | // IsValidClientUUID returns true if the given client uuid is valid. | ||||||
|  | func IsValidClientUUID(uuid string) bool { | ||||||
|  | 	if len(uuid) != UUIDLength { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, c := range uuid { | ||||||
|  | 		valid := (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') | ||||||
|  | 		if !valid { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
| @@ -2,7 +2,7 @@ package onepassword | |||||||
|  |  | ||||||
| import corev1 "k8s.io/api/core/v1" | import corev1 "k8s.io/api/core/v1" | ||||||
|  |  | ||||||
| func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]bool) bool { | func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret) bool { | ||||||
| 	for i := 0; i < len(volumes); i++ { | 	for i := 0; i < len(volumes); i++ { | ||||||
| 		if secret := volumes[i].Secret; secret != nil { | 		if secret := volumes[i].Secret; secret != nil { | ||||||
| 			secretName := secret.SecretName | 			secretName := secret.SecretName | ||||||
| @@ -14,3 +14,16 @@ func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]bool) bo | |||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func AppendUpdatedVolumeSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { | ||||||
|  | 	for i := 0; i < len(volumes); i++ { | ||||||
|  | 		if secret := volumes[i].Secret; secret != nil { | ||||||
|  | 			secretName := secret.SecretName | ||||||
|  | 			secret, ok := secrets[secretName] | ||||||
|  | 			if ok { | ||||||
|  | 				updatedDeploymentSecrets[secret.Name] = secret | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return updatedDeploymentSecrets | ||||||
|  | } | ||||||
|   | |||||||
| @@ -2,12 +2,14 @@ package onepassword | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestAreVolmesUsingSecrets(t *testing.T) { | func TestAreVolmesUsingSecrets(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]bool{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": true, | 		"onepassword-database-secret": &corev1.Secret{}, | ||||||
| 		"onepassword-api-key":         true, | 		"onepassword-api-key":         &corev1.Secret{}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	volumeSecretNames := []string{ | 	volumeSecretNames := []string{ | ||||||
| @@ -24,9 +26,9 @@ func TestAreVolmesUsingSecrets(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func TestAreVolumesNotUsingSecrets(t *testing.T) { | func TestAreVolumesNotUsingSecrets(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]bool{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": true, | 		"onepassword-database-secret": &corev1.Secret{}, | ||||||
| 		"onepassword-api-key":         true, | 		"onepassword-api-key":         &corev1.Secret{}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	volumeSecretNames := []string{ | 	volumeSecretNames := []string{ | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								pkg/utils/k8sutil.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								pkg/utils/k8sutil.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ForceRunModeEnv = "OSDK_FORCE_RUN_MODE" | ||||||
|  |  | ||||||
|  | type RunModeType string | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	LocalRunMode   RunModeType = "local" | ||||||
|  | 	ClusterRunMode RunModeType = "cluster" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ErrNoNamespace indicates that a namespace could not be found for the current | ||||||
|  | // environment | ||||||
|  | var ErrNoNamespace = fmt.Errorf("namespace not found for current environment") | ||||||
|  |  | ||||||
|  | // ErrRunLocal indicates that the operator is set to run in local mode (this error | ||||||
|  | // is returned by functions that only work on operators running in cluster mode) | ||||||
|  | var ErrRunLocal = fmt.Errorf("operator run mode forced to local") | ||||||
|  |  | ||||||
|  | // GetOperatorNamespace returns the namespace the operator should be running in. | ||||||
|  | func GetOperatorNamespace() (string, error) { | ||||||
|  | 	if isRunModeLocal() { | ||||||
|  | 		return "", ErrRunLocal | ||||||
|  | 	} | ||||||
|  | 	nsBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") | ||||||
|  | 	if err != nil { | ||||||
|  | 		if os.IsNotExist(err) { | ||||||
|  | 			return "", ErrNoNamespace | ||||||
|  | 		} | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	ns := strings.TrimSpace(string(nsBytes)) | ||||||
|  | 	return ns, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isRunModeLocal() bool { | ||||||
|  | 	return os.Getenv(ForceRunModeEnv) == string(LocalRunMode) | ||||||
|  | } | ||||||
| @@ -1,5 +1,10 @@ | |||||||
| package utils | package utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
| func ContainsString(slice []string, s string) bool { | func ContainsString(slice []string, s string) bool { | ||||||
| 	for _, item := range slice { | 	for _, item := range slice { | ||||||
| 		if item == s { | 		if item == s { | ||||||
| @@ -18,3 +23,11 @@ func RemoveString(slice []string, s string) (result []string) { | |||||||
| 	} | 	} | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func StringToBool(str string) (bool, error) { | ||||||
|  | 	restartDeploymentBool, err := strconv.ParseBool(strings.ToLower(str)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, err | ||||||
|  | 	} | ||||||
|  | 	return restartDeploymentBool, nil | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										104
									
								
								scripts/prepare-release.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										104
									
								
								scripts/prepare-release.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | #!/usr/bin/env bash | ||||||
|  | # | ||||||
|  | # prepare-release.sh | ||||||
|  | # (Note: This should be called by `make release/prepare` because it depends | ||||||
|  | #   on several variables set by the Makefile) | ||||||
|  | # | ||||||
|  | # Performs release preparation tasks: | ||||||
|  | #   - Creates a release branch | ||||||
|  | #   - Renames "LATEST" section to the new version number | ||||||
|  | #   - Adds new "LATEST" entry to the changelog | ||||||
|  | # | ||||||
|  | ############################################## | ||||||
|  | set -Eeuo pipefail | ||||||
|  |  | ||||||
|  | if [[ -z "${NEW_VERSION:-}" ]]; then | ||||||
|  |     echo "[ERROR] NEW_VERSION environment variable not defined." >&2 | ||||||
|  |     exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Script called from within a git repo? | ||||||
|  | if [[ $(git rev-parse --is-inside-work-tree &>/dev/null) -ne 0 ]]; then | ||||||
|  |     echo "[ERROR] Current directory (${SRCDIR}) is not a git repository" >&2 | ||||||
|  |     exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | REPO_ROOT=$(git rev-parse --show-toplevel) | ||||||
|  | CHANGELOG_FILENAME=${CHANGELOG:-"CHANGELOG.md"} | ||||||
|  |  | ||||||
|  | # normalize version by removing `v` prefix | ||||||
|  | VERSION_NUM=${NEW_VERSION/#v/} | ||||||
|  | RELEASE_BRANCH=$(printf "release/v%s" "${VERSION_NUM}") | ||||||
|  |  | ||||||
|  | function updateChangelog() { | ||||||
|  |     local tmpfile | ||||||
|  |  | ||||||
|  |     trap '[ -e "${tmpfile}" ] && rm "${tmpfile}"' RETURN | ||||||
|  |  | ||||||
|  |     local changelogFile | ||||||
|  |     changelogFile=$(printf "%s/%s" "${REPO_ROOT}" "${CHANGELOG_FILENAME}") | ||||||
|  |  | ||||||
|  |     # create Changelog file if not exists | ||||||
|  |     if ! [[ -f "${REPO_ROOT}/${CHANGELOG_FILENAME}" ]]; then | ||||||
|  |         touch "${REPO_ROOT}/${CHANGELOG_FILENAME}" && \ | ||||||
|  |         git add "${REPO_ROOT}/${CHANGELOG_FILENAME}" | ||||||
|  |     fi | ||||||
|  |  | ||||||
|  |     tmpfile=$(mktemp) | ||||||
|  |  | ||||||
|  |     # Replace "Latest" in the top-most changelog block with new version | ||||||
|  |     # Then push a new "latest" block to top of the changelog | ||||||
|  |     awk 'NR==1, /---/{ sub(/START\/LATEST/, "START/v'${VERSION_NUM}'"); sub(/# Latest/, "# v'${VERSION_NUM}'") } {print}' \ | ||||||
|  |      "${changelogFile}" > "${tmpfile}" | ||||||
|  |  | ||||||
|  |     # Inserts "Latest" changelog HEREDOC at the top of the file | ||||||
|  |     cat - "${tmpfile}" << EOF > "${REPO_ROOT}/${CHANGELOG_FILENAME}" | ||||||
|  | [//]: # (START/LATEST) | ||||||
|  | # Latest | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |   * A user-friendly description of a new feature. {issue-number} | ||||||
|  |  | ||||||
|  | ## Fixes | ||||||
|  |  * A user-friendly description of a fix. {issue-number} | ||||||
|  |  | ||||||
|  | ## Security | ||||||
|  |  * A user-friendly description of a security fix. {issue-number} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | EOF | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function _main() { | ||||||
|  |  | ||||||
|  |     # Stash version changes | ||||||
|  |     git stash push &>/dev/null | ||||||
|  |  | ||||||
|  |     if ! git checkout -b "${RELEASE_BRANCH}" origin/"${MAIN_BRANCH:-main}"; then | ||||||
|  |         echo "[ERROR] Could not check out release branch." >&2 | ||||||
|  |         git stash pop &>/dev/null | ||||||
|  |         exit 1 | ||||||
|  |     fi | ||||||
|  |  | ||||||
|  |     # Add the version changes to release branch | ||||||
|  |     git stash pop &>/dev/null | ||||||
|  |  | ||||||
|  |     updateChangelog | ||||||
|  |  | ||||||
|  |     cat << EOF | ||||||
|  |  | ||||||
|  | [SUCCESS] Changelog updated & release branch created: | ||||||
|  |     New Version:    ${NEW_VERSION} | ||||||
|  |     Release Branch: ${RELEASE_BRANCH} | ||||||
|  |  | ||||||
|  | Next steps: | ||||||
|  |     1. Edit the changelog notes in ${CHANGELOG_FILENAME} | ||||||
|  |     2. Commit changes to the release branch | ||||||
|  |     3. Push changes to remote => git push origin ${RELEASE_BRANCH} | ||||||
|  |  | ||||||
|  | EOF | ||||||
|  |     exit 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | _main | ||||||
							
								
								
									
										5
									
								
								tools.go
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								tools.go
									
									
									
									
									
								
							| @@ -1,5 +0,0 @@ | |||||||
| // +build tools |  | ||||||
|  |  | ||||||
| // Place any runtime dependencies as imports in this file. |  | ||||||
| // Go modules will be forced to download and install them. |  | ||||||
| package tools |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user