Compare commits

..

310 Commits

Author SHA1 Message Date
Volodymyr Zotov
bf6cac81cb Add capabilities for ["CHOWN", "FOWNER"] to make it more striker 2025-09-04 11:17:38 -05:00
Volodymyr Zotov
9c4849ec2e Ignore these files across entire project 2025-09-04 10:43:55 -05:00
Volodymyr Zotov
c2788770fd Add comment about installing 1p cli in test workflow 2025-09-04 10:41:25 -05:00
Volodymyr Zotov
6baef1b9cf Fix lint error 2025-08-28 13:24:55 -05:00
Volodymyr Zotov
7e08158d2f Use op.ReadItemField command 2025-08-28 13:22:11 -05:00
Volodymyr Zotov
976909c438 Update OpReadField method to be able to read different item fields 2025-08-28 13:20:45 -05:00
Volodymyr Zotov
e61ba49018 Add namespace package 2025-08-28 13:17:27 -05:00
Volodymyr Zotov
6492b3cf34 Remove operator package, as make commands can be run directly using system.Run 2025-08-28 13:12:48 -05:00
Volodymyr Zotov
9d08bcc864 Update e2e local testing steps 2025-08-28 11:25:57 -05:00
Volodymyr Zotov
f7f5462133 Pass CRDs on createion of kube instance 2025-08-27 11:19:17 -05:00
Volodymyr Zotov
a1cbd40f9e Refer to testing.md from contributing.md 2025-08-26 16:58:32 -05:00
Volodymyr Zotov
d75a33d524 Add testing.md doc to describe where specific tests should be added 2025-08-26 16:57:42 -05:00
Volodymyr Zotov
b1b6c97a88 Remove redundant if statement 2025-08-26 16:33:49 -05:00
Volodymyr Zotov
0c3caf88b6 Provide default inerval and timeout via config 2025-08-26 16:31:07 -05:00
Volodymyr Zotov
24edff22d4 Do not run e2e tests when moving from draft to ready and vise versa 2025-08-26 16:25:23 -05:00
Volodymyr Zotov
8c893270f4 Update CONTRIBUTING.md with instructions on how to run e2e tests locally 2025-08-26 15:51:59 -05:00
Volodymyr Zotov
d5f1044571 Do not install kubectl cli in pipeline as we use golang library to interact with cluster 2025-08-26 15:12:16 -05:00
Volodymyr Zotov
b40f27b052 Refactor kube package to use controller-runtime golang client to interact with cluster 2025-08-26 15:11:04 -05:00
Volodymyr Zotov
cd03a651ad Refactor kube package to use controller-runtime client instead of using kubectl CLI.
This allows to runt the tests faster
2025-08-25 22:52:17 -05:00
Volodymyr Zotov
9aac824066 Add test case for ignore-secret tag 2025-08-22 11:22:39 -05:00
Volodymyr Zotov
05ad484bd6 Fix lint error 2025-08-22 10:25:45 -05:00
Volodymyr Zotov
71b29d5fe6 Install op-cli into github action job 2025-08-22 10:25:04 -05:00
Volodymyr Zotov
c082f9562e Add tests case to check that kubernetes secret is updated after item is updated in 1Password 2025-08-22 10:15:33 -05:00
Volodymyr Zotov
57478247cf Update secret to point to operator-acceptance-tests vault 2025-08-22 10:13:58 -05:00
Volodymyr Zotov
4836140f66 Add CheckSecretPasswordWasUpdated function to the kube package 2025-08-22 10:13:58 -05:00
Volodymyr Zotov
2b36f16940 Introduce op package to handle op-cli commands 2025-08-22 09:38:21 -05:00
Volodymyr Zotov
bb97134e10 Add comments on each test helper function 2025-08-22 08:30:35 -05:00
Volodymyr Zotov
904d269e7b Roll back changes to customization yaml 2025-08-21 17:01:10 -05:00
Volodymyr Zotov
cf9b267eaf Remove commented code 2025-08-21 16:02:04 -05:00
Volodymyr Zotov
4d64beab86 Exclude e2e tests from make test command 2025-08-21 15:52:38 -05:00
Volodymyr Zotov
ca051a08cf Move testhelper package to pkg so it can be installed as dependency in secrets injector repo 2025-08-21 15:52:06 -05:00
Volodymyr Zotov
22a7c8f586 Create 1password-credentials.json from env var 2025-08-21 14:57:24 -05:00
Volodymyr Zotov
2003d13788 Fix lint issues and CheckSecretExists function 2025-08-21 10:38:19 -05:00
Volodymyr Zotov
7187f41ef1 Checking that all secrets are created before running tests 2025-08-21 10:17:19 -05:00
Volodymyr Zotov
d0b11c70f0 Roll back Connect test 2025-08-21 10:11:06 -05:00
Volodymyr Zotov
9825cb57c9 Test with service account 2025-08-21 10:02:13 -05:00
Volodymyr Zotov
6bb6088353 Use GetProjectRoot to create secret 2025-08-21 09:56:37 -05:00
Volodymyr Zotov
5a56fd3330 Wait for Connect pod is running 2025-08-20 15:51:50 -05:00
Volodymyr Zotov
dcd7eefac0 Increase timeout to 1 minute 2025-08-20 15:39:44 -05:00
Volodymyr Zotov
29b7ed7899 Run correct make command that starts e2e tests 2025-08-20 15:33:08 -05:00
Volodymyr Zotov
331e8d7bfb Add e2e tests workflow 2025-08-20 15:29:58 -05:00
Volodymyr Zotov
c144bd3d01 Remove PatchOperatorManageConnect as manifest has MANAGE_CONNECT: true set already 2025-08-20 15:02:21 -05:00
Volodymyr Zotov
299689fe13 Extract setting context namespace to standalone function SetContextNamespace 2025-08-20 14:57:47 -05:00
Volodymyr Zotov
882d8e951d Never pull the image, but use local when deploying the operator. Deploy along with Connect 2025-08-20 14:56:32 -05:00
Volodymyr Zotov
7885ba649b Set to namespace to default 2025-08-20 14:42:08 -05:00
Volodymyr Zotov
600adf2670 Move cmd package to testhelper and rename to be system 2025-08-20 14:27:12 -05:00
Volodymyr Zotov
88b2dfbf67 Use GetProjectRoot in Run 2025-08-20 14:15:26 -05:00
Volodymyr Zotov
e167db2357 Remove secret from previous step 2025-08-20 10:30:28 -05:00
Volodymyr Zotov
91a9bb6d63 Create op-credentials secret to use operator with Connect 2025-08-20 10:24:16 -05:00
Volodymyr Zotov
116c8c92a7 Update item path to point to test secret 2025-08-20 10:23:44 -05:00
Volodymyr Zotov
4307e9d713 Add 1password-credentials.json and op-session to git ignore
Add 1password-credentials.json to git ignore
2025-08-20 10:23:44 -05:00
Volodymyr Zotov
1759055edd Update sqlite-permissions to run as root, so it can start Connect in e2e tests 2025-08-20 09:10:32 -05:00
Volodymyr Zotov
c1e9934088 Fix typo 2025-08-19 14:51:32 -05:00
Volodymyr Zotov
19b629f2ee Move BuildOperatorImage function to testhelper.operator package 2025-08-19 14:50:39 -05:00
Volodymyr Zotov
174f952691 Split testing flow for Connect and Service Accounts 2025-08-19 12:05:29 -05:00
Volodymyr Zotov
f8704223c8 Move all helpers to testhelper package 2025-08-19 12:04:56 -05:00
Volodymyr Zotov
5630d788a2 Create kube package that abstracts interactions with kubernetes cluster 2025-08-19 11:52:28 -05:00
Volodymyr Zotov
d504e5ef35 Add e2e tests using Service Accounts 2025-08-19 10:51:43 -05:00
Volodymyr Zotov
7d2596a4aa Create e2e tests package 2025-08-15 13:28:39 -05:00
Volodymyr Zotov
f6b267726d Merge pull request #216 from 1Password/vzt/speedup-local-builds
Speedup local builds
2025-07-15 17:12:40 -05:00
Volodymyr Zotov
bf8c1291b2 Update CONTRIBUTING.md after resolving conflicts 2025-07-15 17:09:21 -05:00
Volodymyr Zotov
cd504ec7df Merge branch 'main' into vzt/speedup-local-builds
# Conflicts:
#	Makefile
2025-07-15 17:08:31 -05:00
Eduard Filip
cabc020cc6 Upgrade to Operator SDK 1.41.1 (#211)
* Add missing improvements from Operator SDK 1.34.1

These were not mentioned in the upgrade documentation for version 1.34.x (https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.34.0/), but I've found them by compating the release with the previous one (https://github.com/operator-framework/operator-sdk/compare/v1.33.0...v1.34.1).

* Upgrade to Operator SDK 1.36.0

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.36.0/
Key differences:
- Go packages `k8s.io/*` are already at a version higher than the one in the upgrade.
- `ENVTEST_K8S_VERSION` is at a version higher than the one in the upgrade
- We didn't have the golangci-lint make command before, thus we only needed to add things.

* Upgrade to Operator SDK 1.38.0

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.38.0/

* Upgrade to Operator SDK 1.39.0

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.39.0/

* Upgrade to Operator SDK 1.40.0

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.40.0/

I didn't do the "Add app.kubernetes.io/name label to your manifests" since it seems that we have it already, and it's customized.

* Address lint errors

* Update golangci-lint version used to support Go 1.24

* Improve workflows

- Make workflow targets more specific.
- Make build workflow only build (i.e. remove test part of it).
- Rearrange steps and improve naming for build workflow.

* Add back deleted test

Initially the test has been removed due to lint saying that it was duplicate code, but it falsely errored since the values are different.

* Improve code and add missing upgrade pieces

* Upgrade to Operator SDK 1.41.1

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.41.0/

Upgrading to 1.41.1 from 1.40.0 doesn't have any migration steps.

Key elements:
- Upgrade to golangci-lint v2
- Made the manifests using the updated controller tools

* Address linter errors

golanci-lint v2 seems to be more robust than the previous one, which is beneficial. Thus, we address the linter errors thrown by v2 and improve our code even further.

* Add Makefile improvements

These were brought in by comparing the Makefile of a freshly created operator using the latest operator-sdk with ours.

* Add missing default kustomization for 1.40.0 upgrade

* Bring default kustomization to latest version

This is done by putting the file's content from a newly-generated operator.

* Switch metrics-bind-address default value back to 8080

This ensures that the upgrade is backwards-compatible.

* Add webhook-related scaffolding

This enables us to easily add support for webhooks by running `operator-sdk create webhook` whenever we want to add them.

* Fix typo
2025-07-14 19:32:30 +02:00
Volodymyr Zotov
54eed0c81c Merge pull request #217 from 1Password/release/v1.9.1
Prepare Release - v1.9.1
2025-07-14 10:47:55 -05:00
Volodymyr Zotov
8bd7d519fe Update changelog 2025-07-14 10:44:24 -05:00
Volodymyr Zotov
2823a571e9 Prepare release v1.9.1 2025-07-14 10:37:35 -05:00
Volodymyr Zotov
772e708f02 Merge pull request #212 from 1Password/vzt/fix-panic-when-reading-file
Fix panic when reading file
2025-07-14 10:30:39 -05:00
Volodymyr Zotov
4deb27b853 Update CONTRIBUTING.md 2025-07-10 17:18:00 -05:00
Volodymyr Zotov
75e24e9e47 Introduce docker-image and restart commands
`docker-image` - builds docker image without running tests
`restart` - restarts deployment so all the configuration and code changes are applied to the runing POD
2025-07-10 17:15:27 -05:00
Volodymyr Zotov
583b8251d8 Enable Docker BuildKit for chaching dependencies and faster builds
After enabling it with DOCKER_BUILDKIT=1, it allows to use features like caching(--mount=type=cache) and parallel layer execution which increses speed of concurent builds which is great for local development
2025-07-10 16:36:40 -05:00
Volodymyr Zotov
285066139f Remove '-a' flag from build command to enable caching
'-a' flag forces recompilation of all packages, even if nothing changed, completely bypasses Go’s internal build cache.
2025-07-10 16:33:12 -05:00
Volodymyr Zotov
1d613eac2b Fix logging errors so it won't panic 2025-07-10 15:46:02 -05:00
Volodymyr Zotov
dbd7408fac Update errors text 2025-07-10 15:40:53 -05:00
Volodymyr Zotov
6ef0da2d17 Add file from document 2025-07-10 14:03:57 -05:00
Volodymyr Zotov
b35acb7d13 Add test case for item with file 2025-07-08 16:57:59 -05:00
Volodymyr Zotov
9cee6595d5 Set file content after fetching it 2025-07-08 16:46:10 -05:00
Volodymyr Zotov
24d3f6f043 Retry when getting content file via Connect
Connect has a delay when synchronizing files and returns a 500 error in this case, this function implements a retry mechanism. In this case we handle retries in the code and do not print redundunt errors in the POD logs.
2025-07-08 16:44:09 -05:00
Volodymyr Zotov
5980e7e63a Wrap the error to not lose child errors 2025-07-08 16:42:38 -05:00
Volodymyr Zotov
1e9c04ee05 Fix logging the error so it doesn't panic 2025-07-08 16:41:49 -05:00
Eduard Filip
a5416f4532 Merge pull request #210 from 1Password/eddy/fix-dependabot-alerts
Address dependabot alerts
2025-07-08 19:13:56 +02:00
Eddy Filip
7e739a6fc7 Address dependabot alerts 2025-07-07 22:48:27 +02:00
Volodymyr Zotov
0f1dcdd38a Merge pull request #205 from 1Password/release/v1.9.0
Prepare Release - v1.9.0
2025-06-30 09:56:41 -05:00
Volodymyr Zotov
4c04c6699b Update changleog 2025-06-27 11:17:28 -05:00
Volodymyr Zotov
cd03176aae Prepare release v1.9.0 2025-06-26 19:07:49 -05:00
Volodymyr Zotov
f194485a1b Merge pull request #202 from 1Password/vzt/fix-context
Pass context given in `Reconcile` function down to Connect/SDK requests
2025-06-24 14:21:29 -05:00
Volodymyr Zotov
d1254b06e7 Merge pull request #198 from 1Password/vzt/service-accounts-support
Service accounts support
2025-06-24 14:21:09 -05:00
Volodymyr Zotov
7c84f9d3a4 Remove prerequisites section from USAGEGUIDE.md 2025-06-24 14:18:52 -05:00
Volodymyr Zotov
13abcb9c8f Merge branch 'vzt/service-accounts-support' into vzt/fix-context
# Conflicts:
#	USAGEGUIDE.md
#	cmd/main.go
#	pkg/onepassword/client/client.go
#	pkg/onepassword/items.go
2025-06-17 13:21:39 -05:00
Volodymyr Zotov
b717823fd0 Update examples section 2025-06-17 13:12:58 -05:00
Volodymyr Zotov
c9b969def1 Update comments 2025-06-17 11:27:55 -05:00
Volodymyr Zotov
fd92ef86ab Revert changelog and version change as will be done in release MR
Revert version change as will be dne in release MR
2025-06-17 11:26:04 -05:00
Volodymyr Zotov
842c8febdc Update comment 2025-06-17 11:20:01 -05:00
Volodymyr Zotov
49a5e93c44 Remove GO111MODULE=on as it set to 'on' by default from Go 1.16 2025-06-17 11:19:21 -05:00
Volodymyr Zotov
ac646ec56c Rename vaultIdentifier and itemIdentifier for more clarity 2025-06-17 11:16:57 -05:00
Volodymyr Zotov
458ed24ca3 Check second vault ID 2025-06-16 22:05:06 -05:00
Volodymyr Zotov
bb7b565457 As we check for vault name, we can't initialize the array of exact size as don't know how many items well have ther 2025-06-16 22:04:23 -05:00
Volodymyr Zotov
17d44d90bd Check for empty list 2025-06-16 21:56:51 -05:00
Volodymyr Zotov
0903bacfbd Check Create_At in the test 2025-06-16 21:55:32 -05:00
Volodymyr Zotov
32360d8a83 Remove todos from mocked methods 2025-06-16 21:53:46 -05:00
Volodymyr Zotov
2373fbc87f Updated mapping to be faster 2025-06-16 21:32:00 -05:00
Volodymyr Zotov
704116b855 Remove user agent from Connect config as it's set automatically when initializing Connect client 2025-06-16 21:10:33 -05:00
Volodymyr Zotov
55b5781d7a Pass logger to print what what type of client is used Connect or Service Account 2025-06-16 21:04:22 -05:00
Volodymyr Zotov
1aa27fdba0 Sort imports 2025-06-16 20:56:00 -05:00
Volodymyr Zotov
f164a93b72 Re-arrange env vars 2025-06-16 20:51:16 -05:00
Volodymyr Zotov
9d0736285f Fix type 2025-06-16 20:18:57 -05:00
Volodymyr Zotov
aa1b4ba857 Capitalize 1Password in error 2025-06-16 20:12:38 -05:00
Volodymyr Zotov
ae9b673f96 Remove commented code 2025-06-16 20:11:13 -05:00
Volodymyr Zotov
a0475d7170 Check created_at in the SDK mapper test 2025-06-16 20:09:35 -05:00
Volodymyr Zotov
922f3c8929 Map CreatedAt 2025-06-16 20:07:29 -05:00
Volodymyr Zotov
1fa5bccec2 Upse copy to copy tags 2025-06-16 20:03:36 -05:00
Volodymyr Zotov
cff4d194ba Update constructor function name 2025-06-16 19:45:23 -05:00
Volodymyr Zotov
475860a364 Make changelog description more detailed 2025-06-16 19:43:45 -05:00
Volodymyr Zotov
64aad3d573 Revert version change, as should be done in the release MR 2025-06-16 19:40:54 -05:00
Volodymyr Zotov
0582c2d6e1 Bump go version to 1.24 2025-06-16 19:40:53 -05:00
Volodymyr Zotov
d1be03edd0 Update USAGEGUIDE.md 2025-06-16 19:40:50 -05:00
Volodymyr Zotov
83b294690a Revert version change, as should be done in the release MR 2025-06-13 16:23:45 -05:00
Volodymyr Zotov
ef7361b3c1 Bump go version to 1.24 2025-06-13 16:22:18 -05:00
Volodymyr Zotov
04c1fc1236 Update USAGEGUIDE.md 2025-06-13 16:15:41 -05:00
Volodymyr Zotov
3723c962fe Merge branch 'vzt/service-accounts-support' into vzt/fix-context
# Conflicts:
#	internal/controller/deployment_controller.go
#	internal/controller/onepassworditem_controller.go
2025-06-12 15:08:35 -05:00
Volodymyr Zotov
4d2120061d Bump onepassword-sdk-version 2025-06-12 10:08:55 -05:00
Volodymyr Zotov
c95078c34c Retry after 15 minutes, if get rate-limit error
As currently service account are last for 1 hour https://developer.1password.com/docs/service-accounts/rate-limits/#hourly-limits
2025-06-09 16:04:23 -05:00
Volodymyr Zotov
4527336c37 Increase default container resources
To avoid operator crashing in ~1 min after re-trying unrecoverable error, for example service account rate limit
2025-06-09 16:01:35 -05:00
Volodymyr Zotov
0b6b07b867 Requeue after 1 munute if faced reate limit error from 1password 2025-06-08 12:48:55 -05:00
Volodymyr Zotov
4baad12e10 Add instructions how to use Operator with Service Accounts 2025-06-08 11:23:10 -05:00
Volodymyr Zotov
efbe96e93a Use global context 2025-06-08 10:28:15 -05:00
Volodymyr Zotov
ac06f8db13 Add more logs and fix params order 2025-06-06 16:12:25 -05:00
Volodymyr Zotov
72511ed687 Return error if both Connect and Service Account credentials are provided 2025-06-06 12:56:17 -05:00
Volodymyr Zotov
4757263c66 Wrap errors so it's clear either error is coming from SDK or Connect 2025-06-06 12:53:56 -05:00
Volodymyr Zotov
97e06e5c4d Bump version to 1.9.0 2025-05-30 16:10:25 -05:00
Volodymyr Zotov
a432b42814 Update Docker file to install dependencies and build 2025-05-30 14:52:41 -05:00
Volodymyr Zotov
f88ea6696b Update tests to use testify mock 2025-05-30 14:30:06 -05:00
Volodymyr Zotov
1498c223a5 Use 1Password Client to initialize operator either with Connect or Service Accounts 2025-05-29 17:23:49 -05:00
Volodymyr Zotov
432f2c6cf6 Add Client instance that utilizes either Connect or SDK 2025-05-29 16:06:55 -05:00
Volodymyr Zotov
a49c6ee045 Add SDK client wrapper 2025-05-29 16:06:02 -05:00
Volodymyr Zotov
8881782559 Create Connect client wrapper 2025-05-29 13:12:03 -05:00
Volodymyr Zotov
dcb5d5675a Add internal models
These internal models are introduced to reduce decoupling. The idea is to operate internal model within the project boundaries and convert to appropriate Connect or SDK models in the places where it's necessary.
2025-05-29 11:30:17 -05:00
Volodymyr Zotov
b567b99774 Merge pull request #196 from 1Password/vzt/remove-vendor
Remove vendor folder
2025-05-28 09:06:49 -05:00
Volodymyr Zotov
02c90d424b Remove vendor folder 2025-05-27 14:56:57 -05:00
Kevin Gottsman
4428515407 Update README.md to show the apiVersion (#194)
Typo which doesn't show the apiVersion resulting in an error
2025-02-05 13:55:51 +01:00
Eduard Filip
949a840779 Update bug bounty process (#193) 2024-12-12 18:16:35 +01:00
Andi Titu
ced45c33d4 Merge pull request #192 from 1Password/AndyTitu/cpu-resource-request
Use requests instead of limits for cpu resource
2024-10-09 11:52:11 +03:00
Andi Titu
4091f80351 Use 0.2 instead of 0.2m 2024-10-09 11:45:20 +03:00
Andi Titu
c94e7a5557 use 0.2 instead of 0.2m 2024-10-09 11:44:53 +03:00
Andi Titu
3fbd0b32cd Use requests instead of limits for cpu resource 2024-10-09 11:35:32 +03:00
Simon Barendse
2c55fbc5ed Merge pull request #189 from plttn/patch-1
Fix typo in readme
2024-08-01 17:13:19 +02:00
Jack Platten
fcb97e1482 Update README.md
fix typo
2024-06-11 13:23:05 -07:00
Eduard Filip
b3346cbc25 Run 1Password/check-signed-commits-action for PRs (#188)
Add the 1Password/check-signed-commits-action that will leave a handy comment if a PR contains commits that are not signed.
2024-05-28 19:36:34 +02:00
Eduard Filip
8c0f1a7837 Add CONTRIBUTING.md (#186)
This provides the necessary steps for external contributors to build, test and add features to the operator.
2024-03-25 15:58:16 +01:00
Eduard Filip
eda5612827 Upgrade Operator SDK to v1.34.1 and update dependencies (#185)
This does the following updates:

* Upgrade to Operator SDK v1.34.1. This fixes building multi-arch images from Makefile. Check this MR from operator-framework for details.
* Update Go dependencies. This addresses Dependabot alert ["Golang protojson.Unmarshal function infinite loop when unmarshaling certain forms of invalid JSON"](https://github.com/1Password/onepassword-operator/security/dependabot/13).
* Update versions of the GitHub Actions used in the pipelines.
* Update Kubernetes related tools (such as controller-tools version, and operator-sdk for ci pipelines)

By updating dependencies, the pipelines no longer fail due to a panic error when running `make test`.
2024-03-25 15:41:18 +01:00
github-actions[bot]
5f232b121a Prepare release 1.8.1 (#183)
Co-authored-by: Eddy Filip <eddy.filip@agilebits.com>
2024-01-29 12:32:24 +01:00
Eduard Filip
f72e5243b0 Upgrade the operator to use Operator SDK v1.33.0 (#182)
* Move controller package inside internal directory

Based on the go/v4 project structure, the following changed:
- Pakcage `controllers` is now named `controller`
- Package `controller` now lives inside new `internal` directory

* Move main.go in cmd directory

Based on the new go/v4 project structure, `main.go` now lives in the `cmd` directory.

* Change package import in main.go

* Update go mod dependencies

Update the dependencies based on the versions obtained by creating a new operator project using `kubebuilder init --domain onepassword.com --plugins=go/v4`.

This is based on the migration steps provided to go from go/v3 to go/v4 (https://book.kubebuilder.io/migration/migration_guide_gov3_to_gov4)

* Update vendor

* Adjust code for breaking changes from pkg update

sigs.k8s.io/controller-runtime package had breaking changes from v0.14.5 to v0.16.3. This commit brings the changes needed to achieve the same things using the new functionality avaialble.

* Adjust paths to connect yaml files

Since `main.go` is now in `cmd` directory, the paths to the files for deploying Connect have to be adjusted based on the new location `main.go` is executed from.

* Update files based on new structure and scaffolding

These changes are made based on the new project structure and scaffolding obtained when using the new go/v4 project structure.

These were done based on the migration steps mentioned when migrating to go/v4 (https://book.kubebuilder.io/migration/migration_guide_gov3_to_gov4).

* Update config files

These updates are made based on the Kustomize v4 syntax.

This is part of the upgrate to go/v4 (https://book.kubebuilder.io/migration/migration_guide_gov3_to_gov4)

* Update dependencies and GO version

* Update vendor

* Update Kubernetes tools versions

* Update operator version in Makefile

Now the version in the Makefile matches the version of the operator

* Update Operator SDK version in version.go

* Adjust generated deepcopy

It seems that the +build tag is no longer needed based on the latest generated scaffolding, therefore it's removed.

* Update copyright year

* Bring back missing changes from migration

Some customization in Makefile was lost during the migration process. Specifically, the namespace customization for `make deploy` command.

Also, we push changes to kustomization.yaml for making the deploy process smoother.

* Add RBAC perms for coordination.k8s.io

It seems that with the latest changes to Kubernetes and Kustomize, we need to add additional RBAC to the service account used so that it can properly access the `leases` resource.

* Optimize Dockerfile

Dockerfile had a step for caching dependencies (go mod download). However, this is already done by the vendor directory, which we include. Therefore, this step can be removed to make the image build time faster.
2024-01-25 14:21:31 +01:00
Jillian W
8fc852a4dd Merge pull request #173 from 1Password/release/v1.8.0
Preparing release v1.8.0
2023-08-21 11:18:11 -03:00
jillianwilson
e6998497a2 Updating version in version.go 2023-08-21 11:11:02 -03:00
jillianwilson
4b36cd80bd Preparing release v1.8.0 2023-08-21 10:59:13 -03:00
Jillian W
030d451c94 Merge pull request #170 from mmorejon/add-volumes-projected-detection
Add volumes projected detection
2023-08-15 11:08:41 -03:00
Manuel Morejon
1e73bc1220 refactor volume functions
Signed-off-by: Manuel Morejon <manuel@mmorejon.io>
2023-08-15 01:30:41 +02:00
Jillian W
a42a96bd26 Merge pull request #171 from 1Password/release/v1.7.1
Preparing release
2023-08-14 16:03:18 -03:00
jillianwilson
c8fe537ad1 Preparing release 2023-08-14 14:50:13 -03:00
Manuel Morejon
9b4d8eb292 feat: add volumes projected detection
Signed-off-by: Manuel Morejon <manuel@mmorejon.io>
2023-08-11 02:29:32 +02:00
Jillian W
91c3422597 Merge pull request #167 from 1Password/improve-logging
Adjusting logging level on various logs
2023-08-03 15:43:16 -03:00
jillianwilson
d3d0cfa281 Converting logging enums to constants 2023-08-03 15:39:33 -03:00
Jillian W
5c41962aea Update USAGEGUIDE.md
Co-authored-by: Dustin Ruetz <26169612+dustin-ruetz@users.noreply.github.com>
2023-08-03 15:34:00 -03:00
Jillian W
4413e61f2a Update USAGEGUIDE.md
Co-authored-by: Dustin Ruetz <26169612+dustin-ruetz@users.noreply.github.com>
2023-08-03 15:33:51 -03:00
jillianwilson
63e3cd15fb Noving log levels to variables 2023-08-03 14:31:39 -03:00
jillianwilson
ffb9a4f22a removing test logs 2023-08-03 13:10:11 -03:00
jillianwilson
10cfb55350 Adjusting logging level on various logs 2023-08-02 16:44:01 -03:00
Jillian W
372a5f4aa9 Merge pull request #166 from 1Password/readme-updates
Cleaning up readme and documentation
2023-07-24 10:42:14 -03:00
Jillian W
3bb2f0e9d3 Update USAGEGUIDE.md
Co-authored-by: Eduard Filip <eddy.filip@agilebits.com>
2023-07-24 10:42:08 -03:00
jillianwilson
a78b197db8 Removing commented out docs 2023-07-13 14:59:17 -03:00
jillianwilson
514ef95330 Making improvments to the docs based on pr suggestions 2023-07-13 14:56:24 -03:00
jillianwilson
55922b52b7 Removing redundant quotes from readme 2023-07-12 16:03:34 -03:00
jillianwilson
0c0a498726 Making changes based on PR suggestions 2023-07-12 16:02:26 -03:00
jillianwilson
3d05fcc0d7 Cleaning up readme and documentation 2023-07-11 16:59:45 -03:00
Jillian W
4c9801322b Merge pull request #163 from 1Password/release/v1.7.0
Preparing release
2023-06-16 12:29:40 -03:00
jillianwilson
26ff2408ba Preparing release 2023-06-16 11:09:49 -03:00
Eduard Filip
2dbfc7ecdd Merge pull request #162 from 1Password/update-operator-go-sdk
Upgrade operator go sdk to 1.29.0
2023-06-15 20:43:29 +02:00
Eddy Filip
aaddfd0c79 Upgrade to operator sdk 1.29.0 2023-06-15 20:25:51 +02:00
Jillian W
e72705e9fa Merge pull request #161 from 1Password/update-dependencies
Updating Golang and connect sdk versions
2023-06-15 08:36:51 -03:00
Jillian W
c2d5c835c1 Merge pull request #141 from bdsoha/patch-1
Spell checking
2023-06-14 16:14:20 -03:00
jillianwilson
e4b945ed56 Updating Golang and connect sdk versions 2023-06-14 16:02:06 -03:00
Eduard Filip
50862a8321 Merge pull request #153 from 1Password/update-dependencies
Update dependencies
2023-06-14 17:28:39 +02:00
Eddy Filip
c7548af5c3 Properly use new github step output syntax 2023-06-14 17:22:52 +02:00
Eddy Filip
d00fc40e90 Run 'go mod tidy' 2023-03-24 18:16:39 +01:00
Eddy Filip
802e7c5b56 Update GitHub Action pipelines
Update dependencies and change the syntax of setting step output since set-output command is deprecated
2023-03-24 18:02:27 +01:00
Eddy Filip
63dcaac407 Update Go version and package dependencies 2023-03-24 17:58:25 +01:00
volodymyrZotov
fe930fef05 Merge pull request #151 from 1Password/fix/security_vulnerabilities
Add runAsNonRoot: true and allowPrivilegeEscalation: false to the specs
2023-03-01 08:20:27 -06:00
Volodymyr Zotov
702974f750 Add runAsNonRoot: true and allowPrivilegeEscalation: false to the specs
Signed-off-by: Volodymyr Zotov <volodymyr.zotov@gmail.com>
2023-02-28 15:59:17 -06:00
Eduard Filip
ea8773bfee Merge pull request #146 from 1Password/fix/code-cleanup
Code cleanup
2023-01-05 13:30:31 +01:00
Eddy Filip
a84b5337ea Don't redefine the err variable
Since we take immediate action when getting the error of a function, we can stop redefining it in following steps and use one err variable in the entire function.
2023-01-04 14:09:04 +02:00
Eddy Filip
cd1c978d18 Remove duplicated code for deleting k8s secret and handle an error 2023-01-04 14:05:33 +02:00
Dov Benyomin Sohacheski
34b8f9ee3d spell check 2022-11-24 11:10:20 +02:00
github-actions[bot]
03fa9adf6b Prepare Release - v1.6.0 (#140) 2022-11-11 17:15:13 +01:00
Eduard Filip
672396716d Merge pull request #124 from 1Password/feature/migrate-operator
Migrate operator to the latest operator-sdk
2022-11-10 19:07:30 +01:00
volodymyrZotov
08baab7218 Merge pull request #139 from 1Password/feature/migrate-operator-rename-deployment
Feature/migrate operator rename deployment
2022-11-01 16:00:03 +02:00
volodymyrZotov
cb48c9c902 remove imagePullPolicy from manager.yaml 2022-10-31 18:19:59 +02:00
volodymyrZotov
b05c0661a0 changed label 2022-10-31 18:07:53 +02:00
volodymyrZotov
67330ceeed change default controller name 2022-10-31 17:36:14 +02:00
volodymyrZotov
24ac4fdc9e Merge pull request #138 from 1Password/fix/version-generation-in-tests
generate random version each time calling mock onepassword item
2022-10-26 16:25:38 +03:00
Eddy Filip
710de1bbc0 Adjust OnePasswordItem test resource names
This is made to eliminate the chance of tests being flaky when run in aprallel
2022-10-26 15:18:34 +02:00
volodymyrZotov
d99abbd432 use unique onepassword name in each spec 2022-10-26 11:31:54 +03:00
Eduard Filip
cc26230824 Merge pull request #137 from 1Password/eddy/fix-pipeline
Adjust pipeline based on new actions
2022-10-25 17:39:53 +02:00
volodymyrZotov
30a1c136dc generate random version each time calling mock onepassword item 2022-10-25 18:00:41 +03:00
Eddy Filip
245ec1bcec Adjust syntax based on latest action version 2022-10-25 14:03:07 +02:00
Eddy Filip
c72174f743 Add license 2022-10-24 20:15:32 +02:00
Eduard Filip
be3fdaa34e Merge pull request #136 from 1Password/eddy/update-pipelines
Update pipelines with latest versions of actions
2022-10-24 19:11:43 +02:00
Eddy Filip
ef40618af7 Update actions versions 2022-10-24 17:28:39 +02:00
Eduard Filip
5ced1c4e97 Merge pull request #135 from 1Password/eddy/update-operator-sdk-version
Update operator-sdk version
2022-10-24 17:13:43 +02:00
Eddy Filip
fc1044aaab Remove namespace resource
This is done so that we don't delete the namespace when running `make undeploy`
2022-10-24 15:51:55 +02:00
Eddy Filip
c6c03ca157 Adjust README 2022-10-24 15:45:32 +02:00
Eddy Filip
d75b029dfa Update operator based on latest operator-sdk 2022-10-24 15:36:56 +02:00
volodymyrZotov
a132741778 Merge pull request #126 from 1Password/feature/controllers_tests
Feature/controllers tests
2022-10-24 15:15:32 +03:00
Eddy Filip
d8bfa318f2 Adjust code 2022-10-24 14:09:17 +02:00
Eddy Filip
1d1d824ff4 Adjust context 2022-10-24 13:57:31 +02:00
volodymyrZotov
47922b05e2 Merge pull request #133 from 1Password/migrate-operator-updates
Migrate operator updates
2022-10-24 13:59:04 +03:00
volodymyrZotov
2712e9ce7b Use custom image name for the manager controller 2022-10-24 11:52:19 +03:00
volodymyrZotov
20f81f5b0f update tests 2022-10-20 11:08:56 +03:00
volodymyrZotov
c8022336da improve README.md 2022-10-19 18:08:38 +03:00
volodymyrZotov
c4fdcc6f5f update README.md 2022-10-19 17:44:22 +03:00
volodymyrZotov
916015cd75 use controller image name to build the manager 2022-10-19 17:22:03 +03:00
volodymyrZotov
16d2101da8 use current kubectl namespace to deploy operator 2022-10-19 17:13:05 +03:00
volodymyrZotov
1a8bd75bc8 refactor tests 2022-09-21 13:24:24 +03:00
volodymyrZotov
256b1e09fd added tests for deployment_controller 2022-09-21 12:09:40 +03:00
volodymyrZotov
11b1eae4e1 cover onepassworditem_controller with tests 2022-09-19 18:49:57 +03:00
volodymyrZotov
108cdac29b add "Should not update K8s secret testcase" 2022-09-15 18:08:56 +03:00
Eddy Filip
91eb658d70 Add TODOs for missing tests that need to be migrated 2022-09-13 20:08:10 +03:00
Eddy Filip
c3094dbef0 Migrate README 2022-09-13 19:58:18 +03:00
Eddy Filip
e582d33b45 Migrate remaining files from the old version of the repo
- Migrate `CHANGELOG.md`, `.VERSION`, `LICENSE`, GitHub pipelines, release script, GitHub issue templates.
- Add in Makefile the commands to prepare release and make the release tag.
2022-09-13 16:54:37 +03:00
Eddy Filip
87ff93daad Add some tests for both the API Resource Controller and the Deployment Controller 2022-09-13 16:34:36 +03:00
Eddy Filip
5e496d2e77 Upgrade to GinkGo v2
Ginkgo has switched to v2 and we should make use of it instead. It doesn't affect how we make tests, but we get the latest enhancements and improvements on the ways tests are executed.
2022-09-13 16:31:08 +03:00
Eddy Filip
1d75f78891 Update packages and add vendor directory 2022-09-13 16:24:52 +03:00
Eddy Filip
23b66f73af Update Dockerfile and Makefile 2022-09-13 16:23:01 +03:00
Eddy Filip
e8c380464b Update license in README 2022-09-13 16:19:04 +03:00
Eddy Filip
f1da40aef7 Migrate yaml files.
- `deploy/deployment.yaml` is now `config/manager/manager.yaml`
- `deploy/crds/onepassword.com_v1_onepassworditem_cr.yaml` is now `config/samples/onepassword_v1_onepassworditem.yaml`
2022-09-13 16:18:51 +03:00
Eddy Filip
75501e5b1c Add missing packages
- `version` is used for getting the operator version and the operator SDK version in the logs.
- `k8suitl` is extracted from the operator-sdk to be able to fetch the namespace the operator is deployed to. This is used to deploy Connect in the same namespace if `MANAGE_CONNECT` is set to `true`. In the future, we may want to no rely on this, since this functionality is internal in the operator-sdk.
2022-09-13 16:10:25 +03:00
Eddy Filip
28c3ffade7 Add yaml files for deploying Connect
These yaml files are used when the environment variable `MANAGE_CONNECT` for the operator is set to `true`.
2022-09-13 16:08:26 +03:00
Eddy Filip
946e986048 Migrate main.go 2022-09-13 16:07:06 +03:00
Eddy Filip
250785f4af Migrate Deployment Controller 2022-09-13 16:05:29 +03:00
Eddy Filip
e276ca1148 Clean test code 2022-09-13 15:57:53 +03:00
Eddy Filip
be7b63c37e Update Connect mock client to reflect the latest version of Connect client 2022-09-13 15:56:42 +03:00
Eddy Filip
622fcd64b8 Add packages
- Add the packages that help the operator work as expected.
- Update `go.mod` by running `go mod tidy`.
2022-09-13 15:40:39 +03:00
Eddy Filip
96b78795af Implement the OnePasswordItem Controller
- Migrate the functionality of the Controller for the OnePasswordItem resource.
- Add the RBAC markers that will generate the RBAC permissions.
- Run `make manifests` to generate the RBAC permissions.
2022-09-13 15:21:40 +03:00
Eddy Filip
20b7a2c5cf Implement API and generate manifests
- Define the API by modifying the `onepassworditem_types.go`.
- Commands executed after the API is defined: `make generate && make manifests`.
2022-09-13 15:08:51 +03:00
Eddy Filip
a3de05fbdb Create OnePasswordItem API and controller
Command executed: `operator-sdk create api --version v1 --kind OnePasswordItem --resource --controller`
`--group` flag is skipped because we don't have a group in the previous version of the operator and we want to make things consistent and not bring any breaking changes.
2022-09-13 14:51:50 +03:00
Eddy Filip
a0460ce870 Initialize operator project
Command executed: `operator-sdk init --domain onepassword.com --repo github.com/1Password/onepassword-operator --plugins=go/v4-alpha --license=none`
- `--plugin=go/v4-alpha` is used to enable support for Apple Sillicon environments.
- `--license=none` is used since the only option is Apache, and we use the MIT license. `hack/boilerplate.go.txt` is updated with the MIT license instead.
2022-09-13 14:48:07 +03:00
Eddy Filip
1aa1a3f546 Clear repo
To be able to perform the migration, we need to start from an empty directory/repo.
2022-09-13 12:26:08 +03:00
Joris Coenen
69857c3d47 Merge pull request #119 from 1Password/release/v1.5.0
Release v1.5.0
2022-06-28 14:42:18 +02:00
Joris Coenen
ad276cb296 Fix typo 2022-06-28 11:38:48 +02:00
Joris Coenen
eab5a4ad92 Prepare release v1.5.0 2022-06-28 11:37:17 +02:00
Joris Coenen
128b9b2eb3 Merge pull request #118 from 1Password/item-status
Add Status field to OnePasswordItem resource
2022-06-28 11:28:18 +02:00
Joris Coenen
867e699030 Remove ready field from status
The usage of such a field is considered deprecated, conditions
should be used instead.

If there is a use-case that is not covered by conditions only
we can always reconsider adding an extra field to the status.

See the k8s guidelines for more details on the deprecation:
https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
2022-06-22 11:39:54 +02:00
Joris Coenen
ffab2cfdab Merge remote-tracking branch 'origin/main' into item-status 2022-06-22 11:33:23 +02:00
Joris Coenen
00436b4aee Place back description in CRD
This comment was placed manually and therefore
disappeared when regenerating the CRDs.
2022-06-21 14:38:48 +02:00
Joris Coenen
0ca3415a47 Merge pull request #113 from tim-oster/main
Fix auto deployment restart dropping original pod annotations
2022-06-15 18:21:59 +02:00
Joris Coenen
4aa1f7a669 Merge pull request #109 from slok/slok/opaque-empty-type
Avoid returning an error on secret update when secret types 'Opaque' and 'empty string' are treated as different
2022-06-15 18:03:28 +02:00
Joris Coenen
6c20db47d6 Add Status field to OnePasswordItem resource
This makes it easier to see whehter the controller
succeeded in creating the Kubernetes secret for a
OnePasswordItem. If something failed, the `ready` field
will be `false` and the `OnePasswordItemReady` condition
will have a `status` of `False` with the error messages
in the `message` field.
2022-06-15 17:46:56 +02:00
Tim Oster
874d5c57f9 Fix auto deployment restart dropping original pod annotations 2022-05-16 12:10:13 +02:00
Xabier Larrakoetxea
123cfa2c86 Avoid returning an error on secret update when secret types 'Opaque' and 'empty string' are treated as different
Signed-off-by: Xabier Larrakoetxea <me@slok.dev>
2022-04-14 11:08:51 +02:00
Jillian W
0796b9c5e2 Merge pull request #105 from 1Password/release/v1.4.1
Prepare Release - v1.4.1
2022-04-12 13:49:26 -03:00
Joris Coenen
37a0f4b51e Prepare release v1.4.1 2022-04-12 18:46:06 +02:00
Joris Coenen
004e0101ff Merge pull request #104 from 1Password/secret-annotations
Stop copying annotations from OnePasswordItem and Deployment to Secret
2022-04-12 18:22:24 +02:00
Joris Coenen
6326a856ae Fix test
Annotations are no longer copied from the deployment to the secret,
so the test should not assert that the secret has a name annotation.
2022-04-12 10:41:11 +02:00
Joris Coenen
1ddf92c5a0 Merge branch 'main' into secret-annotations 2022-04-12 10:15:32 +02:00
Joris Coenen
f5c6fa5860 Merge pull request #103 from 1Password/owner-reference-item-update
Persist OwnerReferences when item is updated
2022-04-12 10:14:21 +02:00
Joris Coenen
afa076d321 Stop copying annotations from OnePasswordItem and Deployment to Secret
There is no reason for random annotations to be carried over. This
can lead to weird problems like the `kubectl.kubernetes.io/last-applied-configuration`
annotation ending up on a Secret.
2022-04-11 15:55:28 +02:00
Joris Coenen
d4b04c233c Add missing error checks 2022-04-11 12:12:58 +02:00
Joris Coenen
ea68cfc2b4 Persist OwnerReferences when 1Password item is updated 2022-04-11 12:12:58 +02:00
Jillian W
58b4ff8ecf Merge pull request #99 from 1Password/release/v1.4.0
preparing release 1.4.0
2022-04-07 11:59:20 -03:00
jillianwilson
d93fecdc76 preparing release 1.4.0 2022-04-07 10:23:12 -03:00
Jillian W
486465247d Merge pull request #97 from slok/slok/owner-references
Add owner reference to the created secrets by the operator
2022-04-07 09:19:54 -03:00
Xabier Larrakoetxea
79868ae374 Add owner reference to the created secrets
Signed-off-by: Xabier Larrakoetxea <me@slok.dev>
2022-04-05 20:31:42 +02:00
Marton Soos
6286f7e306 Merge pull request #54 from mcmarkj/secret-path-updates
Deal with itemPath's changing
2022-03-28 15:33:34 +02:00
Marton Soos
0b5efc8690 Merge branch 'main' into secret-path-updates 2022-03-28 15:30:46 +02:00
Marton Soos
c00baeedcb Merge pull request #94 from 1Password/release/v1.3.0
Prepare Release - v1.3.0
2022-03-25 15:51:18 +01:00
Marton Soos
a37bddbfd9 Update release notes 2022-03-25 14:58:07 +01:00
Marton Soos
bd9922f635 Merge pull request #93 from 1Password/feature/file-support
Support loading file fields into Kubernetes secrets
2022-03-24 18:59:28 +01:00
Marton Soos
8fa4413880 Update readme to mention that non-file fields take precedence over file-fields 2022-03-24 18:00:24 +01:00
Marton Soos
62e55a3f19 Update tests and mock client 2022-03-24 12:13:34 +01:00
Marton Soos
d6f7b80c40 Log a message if a file on an item is ignored due to a field with the same name 2022-03-24 11:56:33 +01:00
Marton Soos
a903f9b1af Also add file data to kubernetes secrets 2022-03-24 11:37:24 +01:00
Jillian W
5cddc9d8a9 Merge pull request #92 from 1Password/feature/fix-readme-command
Fix wrong command in readme
2022-03-23 15:54:19 -03:00
Marton Soos
7e1b94fae7 Fix wrong command in readme 2022-03-23 19:36:57 +01:00
Marton Soos
6953a89c89 Merge pull request #91 from AlexVanderbist/patch-1
Fix typos in readme (auto reset -> auto restart)
2022-03-21 12:29:24 +01:00
Marton Soos
0d9e07f543 Update go sdk version to v1.2.0 2022-03-21 11:19:49 +01:00
Alex Vanderbist
098d504d2a Fix typos in readme (auto reset -> auto restart) 2022-03-16 18:21:00 +01:00
Marton Soos
b68d9a5d79 Merge pull request #89 from 1Password/release/v1.2.0
Prepare Release - v1.2.0
2022-02-21 12:25:44 +01:00
Marton Soos
befcaae457 Fix typo in changelog 2022-02-21 11:49:14 +01:00
Marton Soos
b24aa48bd6 Add release notes for v1.2.0 2022-02-21 11:03:14 +01:00
Marton Soos
b1e251dee6 Merge pull request #74 from Nuglif/main
Verify secrets and FromEnv in addition to Env
2022-02-18 20:13:08 +01:00
Marton Soos
a34c6e8b38 Merge pull request #87 from 1Password/feature/kubernetes-secret-types
Feature: Support configuring Kubernetes secret type
2022-02-18 14:16:22 +01:00
Marton Soos
b16960057a Update tests and add new test 2022-02-18 10:47:14 +01:00
Marton Soos
285496dc7e Error when secret type is changed 2022-02-18 10:27:48 +01:00
Marton Soos
f38cf7e1c2 Fix tests and add new test 2022-02-17 21:23:22 +01:00
Marton Soos
bb7a0c8ca9 Simplify secret type cast and default to Opaque 2022-02-17 19:36:49 +01:00
Marton Soos
302653832e Account for the fact that the '' type and Opaque are equivalent on secret comparison 2022-02-17 19:18:33 +01:00
Marton Soos
a1bcfdfdcb Merge branch 'main' into feature/kubernetes-secret-types 2022-02-17 17:54:17 +01:00
Floris van der Grinten
c0f1632638 Merge pull request #72 from samifruit514/main
More logging if 1password item cant be read and continue processing other items
2021-11-18 13:39:34 +01:00
Floris van der Grinten
c46065fa7a Merge branch 'main' into samifruit514/main 2021-11-18 13:29:55 +01:00
Andres Montalban
5d229c42d5 feat: Allow configuration of the Kubernetes Secret type to be created 2021-11-18 08:32:55 -03:00
Joris Coenen
c7235b4f09 Merge pull request #49 from FabioAntunes/patch-1
Update README.md
2021-10-04 12:33:02 +02:00
Joris Coenen
5183fc129a Merge branch 'main' into patch-1 2021-10-04 12:29:48 +02:00
David Gunter
7d619165b2 Merge pull request #76 from Klaudioz/patch-1
Removing $ from bash commands
2021-09-30 09:26:03 -07:00
Claudio Canales
0363ae1e4e Removing $ from bash commands
Using the copy button is bringing the commands with a $, which is giving the error `-bash: $: command not found` after pasting them to the console.
2021-09-29 16:16:45 -03:00
Samuel Archambault
5fab662424 More logging if 1password item cant be read and continue processing others 2021-09-24 11:03:47 -04:00
mcmarkj
a760e524ea Merge branch 'main' of github.com:1Password/onepassword-operator into secret-path-updates 2021-09-13 13:28:25 +01:00
mcmarkj
19f774bb2d Merge branch 'main' of github.com:1Password/onepassword-operator into secret-path-updates 2021-08-19 16:17:57 +01:00
mcmarkj
32643651d9 Fix tests 2021-07-23 15:08:44 +01:00
mcmarkj
ba8d3fa698 Lookup the vaultPath for secrets to check for updates 2021-07-23 13:32:15 +01:00
mcmarkj
c57aa22a9c Update if in the poller 2021-07-22 08:18:52 +01:00
mcmarkj
48944b0d56 Deal with item paths changing 2021-07-22 07:11:50 +01:00
Fábio Antunes
313cd1169b Update README.md
Minor update to the README. Got me debugging for a few hours
2021-07-02 10:23:28 +01:00
2740 changed files with 7840 additions and 889732 deletions

View File

@@ -1 +1 @@
1.1.0
1.9.1

View File

@@ -0,0 +1,25 @@
{
"name": "Kubebuilder DevContainer",
"image": "docker.io/golang:1.24",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/git:1": {}
},
"runArgs": ["--network=host"],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"extensions": [
"ms-kubernetes-tools.vscode-kubernetes-tools",
"ms-azuretools.vscode-docker"
]
}
},
"onCreateCommand": "bash .devcontainer/post-install.sh"
}

View File

@@ -0,0 +1,23 @@
#!/bin/bash
set -x
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
chmod +x ./kind
mv ./kind /usr/local/bin/kind
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/amd64
chmod +x kubebuilder
mv kubebuilder /usr/local/bin/
KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/kubectl
docker network create -d=bridge --subnet=172.19.0.0/24 kind
kind version
kubebuilder version
docker --version
go version
kubectl version --client

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore build and test binaries.
bin/
testbin/

View File

@@ -1,21 +1,22 @@
name: Build and Test
on: [push, pull_request]
name: Build
on:
push:
branches: [main]
pull_request:
jobs:
build:
name: Build
name: Run on Ubuntu
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.15
- name: Clone the code
uses: actions/checkout@v4
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./... -cover

24
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Lint
on:
push:
branches: [main]
pull_request:
jobs:
lint:
name: Run on Ubuntu
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run linter
uses: golangci/golangci-lint-action@v8
with:
version: v2.2

View File

@@ -0,0 +1,13 @@
name: Check signed commits in PR
on: pull_request_target
jobs:
build:
name: Check signed commits in PR
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Check signed commits in PR
uses: 1Password/check-signed-commits-action@v1

View File

@@ -14,9 +14,10 @@ jobs:
outputs:
result: ${{ steps.is_release_branch_without_pr.outputs.result }}
steps:
- id: is_release_branch_without_pr
-
id: is_release_branch_without_pr
name: Find matching PR
uses: actions/github-script@v3
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -27,7 +28,7 @@ jobs:
if(!releaseBranchName) { return false }
const {data: prs} = await github.pulls.list({
const {data: prs} = await github.rest.pulls.list({
...context.repo,
state: 'open',
head: `1Password:${releaseBranchName}`,
@@ -42,19 +43,20 @@ jobs:
name: Create Release Pull Request
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Parse release version
id: get_version
run: echo "::set-output name=version::$(echo $GITHUB_REF | sed 's|^refs/heads/release/v?*||g')"
run: echo "version=$(echo "$GITHUB_REF" | sed 's|^refs/heads/release/v?*||g')" >> $GITHUB_OUTPUT
- 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
DELIMITER="$(openssl rand -hex 8)" # DELIMITER is randomly generated and unique for each run. For more information, see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections.
PR_BODY_CONTENT="
This is an automated PR for a new release.
Please check the following before approving:
@@ -63,14 +65,9 @@ jobs:
---
## 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")"
echo "pr_body<<${DELIMITER}${PR_BODY_CONTENT}${DELIMITER}" >> "${GITHUB_OUTPUT}"
- name: Create Pull Request via API
id: post_pr

View File

@@ -11,15 +11,14 @@ jobs:
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
steps:
-
name: Checkout
uses: actions/checkout@v2
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Docker meta
- name: Docker meta
id: meta
uses: crazy-max/ghaction-docker-meta@v2
uses: docker/metadata-action@v5
with:
images: |
1password/onepassword-operator
@@ -28,24 +27,25 @@ jobs:
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
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker Login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v2
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile

48
.github/workflows/test-e2e.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Test E2E
on:
pull_request:
types: [opened, synchronize, reopened]
branches: ['**'] # run for PRs targeting any branch (main and others)
concurrency:
group: e2e-${{ github.event.pull_request.head.ref }}
cancel-in-progress: true # cancel previous job runs for the same branch
jobs:
e2e-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install dependencies
run: go mod tidy
- name: Create kind cluster
uses: helm/kind-action@v1
with:
cluster_name: onepassword-operator-test-e2e
# install cli to interact with item in 1Password to update/read using `testhelper/op` package
- name: Install 1Password CLI
uses: 1password/install-cli-action@v2
with:
version: 2.32.0
- name: Create '1password-credentials.json' file
env:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
run: |
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
- name: Run E2E tests
run: make test-e2e
env:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}

24
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
name: Run on Ubuntu
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Running Tests
run: |
go mod tidy
make test

92
.gitignore vendored
View File

@@ -1,80 +1,30 @@
# 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
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with 'go test -c'
bin
testbin/*
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
### Vim ###
# swap
.sw[a-p]
.*.sw[a-p]
# session
Session.vim
# temporary
.netrwhist
# auto-generated tag files
tags
### VisualStudioCode ###
.vscode/*
.history
.DS_Store
op-ss-client/
.idea/
# End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode
# Go workspace file
go.work
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*
# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~
**/1password-credentials.json
**/op-session

52
.golangci.yml Normal file
View File

@@ -0,0 +1,52 @@
version: "2"
run:
allow-parallel-runners: true
linters:
default: none
enable:
- copyloopvar
- dupl
- errcheck
- ginkgolinter
- goconst
- gocyclo
- govet
- ineffassign
- lll
- misspell
- nakedret
- prealloc
- revive
- staticcheck
- unconvert
- unparam
- unused
settings:
revive:
rules:
- name: comment-spacings
- name: import-shadowing
exclusions:
generated: lax
rules:
- linters:
- lll
path: api/*
- linters:
- dupl
- lll
path: internal/*
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -12,68 +12,212 @@
---
[//]: # (START/v1.1.0)
[//]: # (START/v1.9.1)
# v1.9.1
## Fixes
* Operator no longer panics when handling 1Password items containing files. {#209}
## Security
* HTTP Proxy bypass using IPv6 Zone IDs in golang.org/x/net. {#210}
* golang.org/x/net vulnerable to Cross-site Scripting. {#210}
---
[//]: # (START/v1.9.0)
# v1.9.0
## Features
* Enable the Operator to authenticate to 1Password using service accounts. {#160}
## Fixes
* Update Operator to use SDK v1.34.1. {#185}
* Pass Kubernetes context down to SDK/Connect. {#199}
---
[//]: # (START/v1.8.1)
# v1.8.1
## Fixes
* Upgrade operator to use Operator SDK v1.33.0. {#180}
---
[//]: # (START/v1.8.0)
# v1.8.0
## Features
* Added volume projected detection. Credit to @mmorejon. {#168}
---
[//]: # (START/v1.7.1)
# v1.7.1
## Fixes
* Adjusting logging level on various logs to reduce unnecessary logging. {#164}
---
[//]: # (START/v1.7.0)
# v1.7.0
## Features
* Upgraded operator to version 1.29.0. {#162}
* Upgraded Golang version to 1.20. {#161}
* Upgraded 1Password Connect version to 1.5.1. {#161}
* Added runAsNonRoot and allowPrivalegeEscalation to specs. {#151}
* Added code quality improvements. {#146}
---
[//]: # (START/v1.6.0)
# v1.6.0
This version of the operator highlights the migration of the operator
to use the latest version of the `operator-sdk` (`1.25.0` at the time of this release).
For the users, this shouldn't affect the functionality of the operator.
This migration enables us to use the new project structure, as well as updated packages that enables
the team (as well as the contributors) to develop the operator more effective.
## Features
* Migrate the operator to use the latest `operator-sdk` {#124}
---
[//]: # (START/v1.5.0)
# v1.5.0
## Features
* `OnePasswordItem` now contains a `status` which contains the status of creating the kubernetes secret for a OnePasswordItem. {#52}
## Fixes
* The operator no longer logs an error about changing the secret type if the secret type is not actually being changed.
* Annotations on a deployment are no longer removed when the operator triggers a restart. {#112}
---
[//]: # "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}
- Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
---
[//]: # (START/v1.0.2)
[//]: # "START/v1.0.2"
# v1.0.2
## Fixes
* Name normalizer added to handle non-conforming item names.
- Name normalizer added to handle non-conforming item names.
---
[//]: # (START/v1.0.1)
[//]: # "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.
- 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)
[//]: # "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
- 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
- Fix spec field example for OnePasswordItem in readme
- Casing of annotations are now consistent
---
[//]: # (START/v0.0.2)
[//]: # "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>`
- Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>`
---
[//]: # (START/v0.0.1)
[//]: # "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
- 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
---

88
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,88 @@
# Contributing
Thank you for your interest in contributing to the 1Password Kubernetes Operator project 👋! Before you start, please take a moment to read through this guide to understand our contribution process.
## Testing
All contributions must include tests where applicable.
- **Unit tests** for pure Go logic.
- **Integration tests** for controller/reconciler logic using envtest.
- **E2E tests** for full cluster behavior with kind.
👉 See the [Testing Guide](docs/testing.md) for details on when to use each, how to run them locally, and how they are run in CI.
----
For functional testing, run the local version of the operator. From the project root:
```sh
# Go to the K8s environment (e.g. minikube)
eval $(minikube docker-env)
# Build the local Docker image for the operator
make docker-build
# Deploy the operator
make deploy
# Remove the operator from K8s
make undeploy
```
- After making changes to the code:
1. Rebuild the Docker image by running `make docker-build`
2. Restart deployment `make restart`
----
- For testing the changes made to the `OnePasswordItem` Custom Resource Definition (CRD), you need to re-generate the object:
```sh
make manifests
```
- Run tests for the operator:
```sh
make test
```
You can check other available commands that may come in handy by running `make help`.
## Debugging
- Running `kubectl describe pod` will fetch details about pods. This includes configuration information about the container(s) and Pod (labels, resource requirements, etc) and status information about the container(s) and Pod (state, readiness, restart count, events, etc.).
- Running `kubectl logs ${POD_NAME} ${CONTAINER_NAME}` will print the logs from the container(s) in a pod. This can help with debugging issues by inspection.
- Running `kubectl exec ${POD_NAME} -c ${CONTAINER_NAME} -- ${CMD}` allows executing a command inside a specific container.
For more debugging documentation, see: https://kubernetes.io/docs/tasks/debug/debug-application/debug-pods/
## Documentation Updates
If applicable, update the [USAGEGUIDE.md](./USAGEGUIDE.md) and [README.md](./README.md) to reflect any changes introduced by the new code.
## Sign your commits
To get your PR merged, we require you to sign your commits. There are three options you can choose from.
### Sign commits with 1Password
You can sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process.
Learn how to use [1Password to sign your commits](https://developer.1password.com/docs/ssh/git-commit-signing/).
### Sign commits with ssh-agent
Follow the steps below to set up commit signing with `ssh-agent`:
1. [Generate an SSH key and add it to ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)
2. [Add the SSH key to your GitHub account](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account)
3. [Configure git to use your SSH key for commits signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-ssh-key)
### Sign commits with gpg
Follow the steps below to set up commit signing with `gpg`:
1. [Generate a GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key)
2. [Add the GPG key to your GitHub account](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account)
3. [Configure git to use your GPG key for commits signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-gpg-key)

View File

@@ -1,31 +1,42 @@
# Build the manager binary
FROM golang:1.13 as builder
FROM golang:1.24 AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# Download dependencies
RUN go mod download
# Copy the go source
COPY cmd/manager/main.go main.go
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/controller/ internal/controller/
COPY pkg/ pkg/
COPY version/ version/
COPY vendor/ vendor/
# Build
ARG operator_version=dev
RUN CGO_ENABLED=0 \
GO111MODULE=on \
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 \
GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build \
-ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \
-mod vendor \
-a -o manager main.go
-o manager cmd/main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER nonroot:nonroot
COPY deploy/connect/ deploy/connect/
USER 65532:65532
COPY config/connect/ config/connect/
ENTRYPOINT ["/manager"]

409
Makefile
View File

@@ -1,8 +1,382 @@
export MAIN_BRANCH ?= main
.DEFAULT_GOAL := help
.PHONY: test build build/binary build/local clean test/coverage release/prepare release/tag .check_bump_type .check_git_clean help
# 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 ?= 1.9.1
# DEPLOYMENT_NAME defines Kubernetes deployment name for the operator.
# It should be the same as in 'config/manager/manager.yaml'
DEPLOYMENT_NAME ?= onepassword-connect-operator
# 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-bundle:$VERSION and onepassword.com/onepassword-operator-catalog:$VERSION.
IMAGE_TAG_BASE ?= onepassword.com/onepassword-operator
# 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
# Set the Operator SDK version to use. By default, what is installed on the system is used.
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
OPERATOR_SDK_VERSION ?= v1.41.1
# Image URL to use all building/pushing image targets
IMG ?= 1password/onepassword-operator:latest
# 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
# CONTAINER_TOOL defines the container tool to be used for building images.
# Be aware that the target commands are only tested with Docker which is
# scaffolded by default. However, you might want to replace it to use other
# tools. (i.e. podman)
CONTAINER_TOOL ?= docker
# Setting SHELL to bash allows bash commands to be executed by recipes.
# 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 command 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 setup-envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $(shell go list ./... | grep -v /test/e2e) -coverprofile cover.out
# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
# CertManager is installed by default; skip with:
# - CERT_MANAGER_INSTALL_SKIP=true
KIND_CLUSTER ?= onepassword-operator-test-e2e
.PHONY: setup-test-e2e
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
@command -v $(KIND) >/dev/null 2>&1 || { \
echo "Kind is not installed. Please install Kind manually."; \
exit 1; \
}
@case "$$($(KIND) get clusters)" in \
*"$(KIND_CLUSTER)"*) \
echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
*) \
echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
esac
.PHONY: test-e2e
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
KIND_CLUSTER=$(KIND_CLUSTER) go test ./test/e2e/ -v -ginkgo.v
$(MAKE) cleanup-test-e2e
.PHONY: cleanup-test-e2e
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
@$(KIND) delete cluster --name $(KIND_CLUSTER)
.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
$(GOLANGCI_LINT) run
.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
$(GOLANGCI_LINT) run --fix
.PHONY: lint-config
lint-config: golangci-lint ## Verify golangci-lint linter configuration
$(GOLANGCI_LINT) config verify
##@ Build
.PHONY: build
build: manifests generate fmt vet ## Build manager binary.
go build -o bin/manager cmd/main.go
.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
go run ./cmd/main.go
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
DOCKER_BUILDKIT=1 $(CONTAINER_TOOL) build -t ${IMG} .
.PHONY: docker-push
docker-push: ## Push docker image with the manager.
$(CONTAINER_TOOL) push ${IMG}
# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
.PHONY: docker-buildx
docker-buildx: ## Build and push docker image for the manager for cross-platform support
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
- $(CONTAINER_TOOL) buildx create --name onepassword-operator-builder
$(CONTAINER_TOOL) buildx use onepassword-operator-builder
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
- $(CONTAINER_TOOL) buildx rm onepassword-operator-builder
rm Dockerfile.cross
.PHONY: build-installer
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
mkdir -p dist
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > dist/install.yaml
##@ 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: set-namespace
set-namespace:
cd config/default && $(KUSTOMIZE) edit set namespace $(shell $(KUBECTL) config view --minify -o jsonpath={..namespace})
.PHONY: deploy
deploy: manifests kustomize set-namespace ## 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: kustomize ## 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 -
.PHONY: restart
restart: ## Restarts deployment so that the operator picks up changes in the deployment configuration.
$(KUBECTL) rollout restart deployment $(DEPLOYMENT_NAME)
##@ Dependencies
## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p $(LOCALBIN)
## Tool Binaries
KUBECTL ?= kubectl
KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
## Tool Versions
KUSTOMIZE_VERSION ?= v5.6.0
CONTROLLER_TOOLS_VERSION ?= v0.18.0
# ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
ENVTEST_VERSION := $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
# ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION := $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
GOLANGCI_LINT_VERSION ?= v2.2.0
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
.PHONY: setup-envtest
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
@$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
exit 1; \
}
.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f $(1) || true ;\
GOBIN=$(LOCALBIN) go install $${package} ;\
mv $(1) $(1)-$(3) ;\
} ;\
ln -sf $(1)-$(3) $(1)
endef
.PHONY: operator-sdk
OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk
operator-sdk: ## Download operator-sdk locally if necessary.
ifeq (,$(wildcard $(OPERATOR_SDK)))
ifeq (, $(shell which operator-sdk 2>/dev/null))
@{ \
set -e ;\
mkdir -p $(dir $(OPERATOR_SDK)) ;\
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\
chmod +x $(OPERATOR_SDK) ;\
}
else
OPERATOR_SDK = $(shell which operator-sdk)
endif
endif
.PHONY: bundle
bundle: manifests kustomize operator-sdk ## 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.
$(CONTAINER_TOOL) 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 = $(LOCALBIN)/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.55.0/$${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 $(CONTAINER_TOOL) --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)
## Release functions =====================
GIT_BRANCH := $(shell git symbolic-ref --short HEAD)
WORKTREE_CLEAN := $(shell git status --porcelain 1>/dev/null 2>&1; echo $$?)
SCRIPTS_DIR := $(CURDIR)/scripts
@@ -10,37 +384,6 @@ SCRIPTS_DIR := $(CURDIR)/scripts
versionFile = $(CURDIR)/.VERSION
curVersion := $(shell cat $(versionFile) | sed 's/^v//')
OPERATOR_NAME := onepassword-connect-operator
DOCKER_IMG_TAG ?= $(OPERATOR_NAME):v$(curVersion)
test: ## Run test suite
go test ./...
test/coverage: ## Run test suite with coverage report
go test -v ./... -cover
build: ## Build operator Docker image
@docker build -f Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG) .
@echo "Successfully built and tagged image."
@echo "Tag: $(DOCKER_IMG_TAG)"
build/local: ## Build local version of the operator Docker image
@docker build -f Dockerfile -t local/$(DOCKER_IMG_TAG) .
build/binary: clean ## Build operator binary
@mkdir -p dist
@go build -mod vendor -a -o manager ./cmd/manager/main.go
@mv manager ./dist
clean:
rm -rf ./dist
help: ## Prints this help message
@grep -E '^[\/a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
## Release functions =====================
release/prepare: .check_git_clean ## Updates changelog and creates release branch (call with 'release/prepare version=<new_version_number>')
@test $(version) || (echo "[ERROR] version argument not set."; exit 1)

22
PROJECT Normal file
View File

@@ -0,0 +1,22 @@
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
domain: onepassword.com
layout:
- go.kubebuilder.io/v4
plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}
projectName: onepassword-operator
repo: github.com/1Password/onepassword-operator
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: onepassword.com
kind: OnePasswordItem
path: github.com/1Password/onepassword-operator/api/v1
version: v1
version: "3"

232
README.md
View File

@@ -1,106 +1,33 @@
# 1Password Connect Kubernetes Operator
<!-- Image sourced from https://blog.1password.com/introducing-secrets-automation/ -->
<img alt="" role="img" src="https://blog.1password.com/posts/2021/secrets-automation-launch/header.svg"/>
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.
<div align="center">
<h1>1Password Connect Kubernetes Operator</h1>
<p>Integrate <a href="https://developer.1password.com/docs/connect">1Password Connect</a> with your Kubernetes Infrastructure</p>
<a href="https://github.com/1Password/onepassword-operator#-get-started">
<img alt="Get started" src="https://user-images.githubusercontent.com/45081667/226940040-16d3684b-60f4-4d95-adb2-5757a8f1bc15.png" height="37"/>
</a>
</div>
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 can be automatically restarted.
The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes Secrets with 1Password. The operator also handles autorestarting deployments when 1Password items are updated.
## Setup
## ✨ Get started
Prerequisites:
### 🚀 Quickstart
- [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/)
- [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [docker installed](https://docs.docker.com/get-docker/)
- [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.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.
1. Add the [1Password Helm Chart](https://github.com/1Password/connect-helm-charts) to your repository.
### Quickstart for Deploying 1Password Connect to Kubernetes
2. Run the following command to install Connect and the 1Password Kubernetes Operator in your infrastructure:
#### 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
```
helm install connect 1password/connect --set-file connect.credentials=1password-credentials-demo.json --set operator.create=true --set operator.token.value = <your connect token>
```
Create a Kubernetes secret from the op-session file:
```bash
3. Create a Kubernetes Secret from a 1Password item:
$ kubectl create secret generic op-credentials --from-file=1password-credentials.json
```
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
**Create Kubernetes Secret for OP_CONNECT_TOKEN**
"Create a Connect token for the operator and save it as a Kubernetes Secret:
```bash
$ 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**
We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder.
```bash
$ kubectl apply -f deploy/permissions.yaml
```
**Create Custom One Password Secret Resource**
```bash
$ kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
```
**Deploying the Operator**
An sample Deployment yaml can be found at `/deploy/operator.yaml`.
To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml:
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 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:
```yaml
kubectl apply -f deploy/operator.yaml
```
## Usage
To create a Kubernetes Secret from a 1Password item, create a yaml file with the following
```yaml
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
@@ -111,125 +38,28 @@ spec:
Deploy the OnePasswordItem to Kubernetes:
```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:
Check that the Kubernetes Secret has been generated:
```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.
### 📄 Usage
To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata:
Refer to the [Usage Guide](USAGEGUIDE.md) for documentation on how to deploy and use the 1Password Operator.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-example
annotations:
operator.1password.io/item-path: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
operator.1password.io/item-name: "<secret_name>"
```
## 💙 Community & Support
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.
- File an [issue](https://github.com/1Password/onepassword-operator/issues) for bugs and feature requests.
- Join the [Developer Slack workspace](https://join.slack.com/t/1password-devs/shared_invite/zt-1halo11ps-6o9pEv96xZ3LtX_VE0fJQA).
- Subscribe to the [Developer Newsletter](https://1password.com/dev-subscribe/).
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 reset 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 reset 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 reset settings on the deployment will be used.
## 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
```shell
make test
```
With coverage:
```shell
make test/coverage
```
## Security
## 🔐 Security
1Password requests you practice responsible disclosure if you discover a vulnerability.
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits).
Please file requests by sending an email to bugbounty@agilebits.com.

221
USAGEGUIDE.md Normal file
View File

@@ -0,0 +1,221 @@
<img alt="" role="img" src="https://blog.1password.com/posts/2021/secrets-automation-launch/header.svg"/>
<div align="center">
<h1>Usage Guide</h1>
</div>
## Table of Contents
1. [Configuration Options](#configuration-options)
2. [Use Kubernetes Operator with Service Account](#use-kubernetes-operator-with-service-account)
- [Create a Service Account](#1-create-a-service-account)
- [Create a Kubernetes secret](#2-create-a-kubernetes-secret-for-the-service-account)
- [Deploy the Operator](#3-deploy-the-operator)
3. [Use Kubernetes Operator with Connect](#use-kubernetes-operator-with-connect)
- [Deploy with Helm](#1-deploy-with-helm)
- [Deploy manually](#2-deploy-manually)
4. [Logging level](#logging-level)
5. [Usage examples](#usage-examples)
6. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets)
7. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
8. [Development](#development)
---
## Configuration options
There are 2 ways 1Password Operator can talk to 1Password servers:
- [1Password Service Accounts](https://developer.1password.com/docs/service-accounts)
- [1Password Connect](https://developer.1password.com/docs/connect/)
---
## Use Kubernetes Operator with Service Account
### 1. [Create a service account](https://developer.1password.com/docs/service-accounts/get-started#create-a-service-account)
### 2. Create a Kubernetes secret for the Service Account
- Set `OP_SERVICE_ACCOUNT_TOKEN` environment variable to the service account token you created in the previous step. This token will be used by the operator to access 1Password items.
- Create Kubernetes secret:
```bash
kubectl create secret generic onepassword-service-account-token --from-literal=token="$OP_SERVICE_ACCOUNT_TOKEN"
```
### 3. Deploy the Operator
An sample Deployment yaml can be found at `/config/manager/manager.yaml`.
To use Operator with Service Account, you need to set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable in the `/config/manager/manager.yaml`. And remove `OP_CONNECT_TOKEN` and `OP_CONNECT_HOST` environment variables.
To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml:
- **OP_SERVICE_ACCOUNT_TOKEN** *(required)*: Specifies Service Account token within Kubernetes to access the 1Password items.
- **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.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password. 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.
To deploy the operator, simply run the following command:
```shell
make deploy
```
**Undeploy Operator**
```
make undeploy
```
---
## Use Kubernetes Operator with Connect
### 1. [Deploy with Helm](https://developer.1password.com/docs/k8s/operator/?deployment-type=helm#helm-step-1)
### 2. [Deploy manually](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#manual-step-1)
To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml:
- **OP_CONNECT_HOST** *(required)*: Specifies the host name within Kubernetes in which to access the 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 current 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.
---
## Logging level
You can set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. The default value is `debug`.
Example:
```yaml
....
containers:
- command:
- /manager
args:
- --leader-elect
- --zap-log-level=info
image: 1password/onepassword-operator:latest
....
```
---
## Usage examples
Find usage [examples](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#usage-examples) on 1Password developer documentation.
---
## How 1Password Items Map to Kubernetes Secrets
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.
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.
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.
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:
- Operator level
- Per-namespace
- Per-deployment
**Operator Level**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environment variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false.
**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
### How it works
This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/)
which provides a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster
### Test It Out
1. Install the CRDs into the cluster:
```sh
make install
```
2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running):
```sh
make run
```
**NOTE:** You can also run this in one step by running: `make install run`
### Modifying the API definitions
If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
```sh
make manifests
```
**NOTE:** Run `make --help` for more information on all potential `make` targets
More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)

View File

@@ -1,6 +1,7 @@
/*
MIT License
Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +20,25 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Package v1 contains API Schema definitions for the v1 API group
// +kubebuilder:object:generate=true
// +groupName=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.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
)

View File

@@ -0,0 +1,95 @@
/*
MIT License
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package v1
import (
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.
// OnePasswordItemSpec defines the desired state of OnePasswordItem
type OnePasswordItemSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
ItemPath string `json:"itemPath,omitempty"`
}
type OnePasswordItemConditionType string
const (
// OnePasswordItemReady means the Kubernetes secret is ready for use.
OnePasswordItemReady OnePasswordItemConditionType = "Ready"
)
type OnePasswordItemCondition struct {
// Type of job condition, Completed.
Type OnePasswordItemConditionType `json:"type"`
// Status of the condition, one of True, False, Unknown.
Status metav1.ConditionStatus `json:"status"`
// Last time the condition transit from one status to another.
// +optional
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
// Human-readable message indicating details about last transition.
// +optional
Message string `json:"message,omitempty"`
}
// OnePasswordItemStatus defines the observed state of OnePasswordItem
type OnePasswordItemStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Conditions []OnePasswordItemCondition `json:"conditions"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// OnePasswordItem is the Schema for the onepassworditems API
type OnePasswordItem struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
Type string `json:"type,omitempty"`
Spec OnePasswordItemSpec `json:"spec,omitempty"`
Status OnePasswordItemStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// OnePasswordItemList contains a list of OnePasswordItem
type OnePasswordItemList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []OnePasswordItem `json:"items"`
}
func init() {
SchemeBuilder.Register(&OnePasswordItem{}, &OnePasswordItemList{})
}

View File

@@ -1,6 +1,30 @@
// +build !ignore_autogenerated
//go:build !ignore_autogenerated
// Code generated by operator-sdk. DO NOT EDIT.
/*
MIT License
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1
@@ -14,8 +38,7 @@ func (in *OnePasswordItem) DeepCopyInto(out *OnePasswordItem) {
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
out.Status = in.Status
return
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem.
@@ -36,6 +59,22 @@ func (in *OnePasswordItem) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OnePasswordItemCondition) DeepCopyInto(out *OnePasswordItemCondition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemCondition.
func (in *OnePasswordItemCondition) DeepCopy() *OnePasswordItemCondition {
if in == nil {
return nil
}
out := new(OnePasswordItemCondition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
*out = *in
@@ -48,7 +87,6 @@ func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemList.
@@ -72,7 +110,6 @@ func (in *OnePasswordItemList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OnePasswordItemSpec) DeepCopyInto(out *OnePasswordItemSpec) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemSpec.
@@ -88,7 +125,13 @@ func (in *OnePasswordItemSpec) DeepCopy() *OnePasswordItemSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) {
*out = *in
return
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]OnePasswordItemCondition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus.

View File

@@ -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}

View File

@@ -1,3 +0,0 @@
#!/bin/sh -e
exec ${OPERATOR} $@

View File

@@ -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"

428
cmd/main.go Normal file
View File

@@ -0,0 +1,428 @@
/*
MIT License
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package main
import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
// 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"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/internal/controller"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/onepassword-operator/version"
// +kubebuilder:scaffold:imports
)
var (
scheme = k8sruntime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
const (
envPollingIntervalVariable = "POLLING_INTERVAL"
manageConnect = "MANAGE_CONNECT"
restartDeploymentsEnvVariable = "AUTO_RESTART"
defaultPollingInterval = 600
annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
)
func printVersion() {
setupLog.Info(fmt.Sprintf("Operator Version: %s", version.OperatorVersion))
setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
setupLog.Info(fmt.Sprintf("Version of operator-sdk: %v", version.OperatorSDKVersion))
}
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(onepasswordcomv1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var metricsCertPath, metricsCertName, metricsCertKey string
var webhookCertPath, webhookCertName, webhookCertKey string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "8080",
"The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
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.")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
"The directory that contains the metrics server certificate.")
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt",
"The name of the metrics server certificate file.")
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key",
"The name of the metrics server key file.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancelation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("disabling http/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
printVersion()
// Create a root context that will be cancelled on termination signals
ctx := ctrl.SetupSignalHandler()
watchNamespace, err := getWatchNamespace()
if err != nil {
setupLog.Error(err, "unable to get WatchNamespace, "+
"the manager will watch and manage resources in all namespaces")
}
deploymentNamespace, err := utils.GetOperatorNamespace()
if err != nil {
setupLog.Error(err, "Failed to get namespace")
os.Exit(1)
}
// Create watchers for metrics and webhooks certificates
var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher
// Initial webhook TLS options
webhookTLSOpts := tlsOpts
if len(webhookCertPath) > 0 {
setupLog.Info("Initializing webhook certificate watcher using provided certificates",
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
var err error
webhookCertWatcher, err = certwatcher.New(
filepath.Join(webhookCertPath, webhookCertName),
filepath.Join(webhookCertPath, webhookCertKey),
)
if err != nil {
setupLog.Error(err, "Failed to initialize webhook certificate watcher")
os.Exit(1)
}
webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) {
config.GetCertificate = webhookCertWatcher.GetCertificate
})
}
webhookServer := webhook.NewServer(webhook.Options{
TLSOpts: webhookTLSOpts,
})
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
// TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are
// not provided, self-signed certificates will be generated by default. This option is not recommended for
// production environments as self-signed certificates do not offer the same level of trust and security
// as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing
// unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName
// to provide certificates, ensuring the server communicates using trusted and secure certificates.
TLSOpts: tlsOpts,
}
if secureMetrics {
// FilterProvider is used to protect the metrics endpoint with authn/authz.
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}
// If the certificate is not specified, controller-runtime will automatically
// generate self-signed certificates for the metrics server. While convenient for development and testing,
// this setup is not recommended for production.
//
// TODO(user): If you enable certManager, uncomment the following lines:
// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
// managed by cert-manager for the metrics server.
// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
if len(metricsCertPath) > 0 {
setupLog.Info("Initializing metrics certificate watcher using provided certificates",
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
var err error
metricsCertWatcher, err = certwatcher.New(
filepath.Join(metricsCertPath, metricsCertName),
filepath.Join(metricsCertPath, metricsCertKey),
)
if err != nil {
setupLog.Error(err, "Failed to initialize metrics certificate watcher")
os.Exit(1)
}
metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) {
config.GetCertificate = metricsCertWatcher.GetCertificate
})
}
options := ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "c26807fd.onepassword.com",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
}
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
if watchNamespace != "" {
namespaces := strings.Split(watchNamespace, ",")
namespaceMap := make(map[string]cache.Config)
for _, namespace := range namespaces {
namespaceMap[namespace] = cache.Config{}
}
options.NewCache = func(config *rest.Config, opts cache.Options) (cache.Cache, error) {
opts.DefaultNamespaces = namespaceMap
return cache.New(config, opts)
}
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
// Setup One Password Client
opClient, err := opclient.NewFromEnvironment(ctx, opclient.Config{
Logger: setupLog,
Version: version.OperatorVersion,
})
if err != nil {
setupLog.Error(err, "unable to create 1Password client")
os.Exit(1)
}
if err = (&controller.OnePasswordItemReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
OpClient: opClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem")
os.Exit(1)
}
r, _ := regexp.Compile(annotationRegExpString)
if err = (&controller.DeploymentReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
OpClient: opClient,
OpAnnotationRegExp: r,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Deployment")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
// Setup 1PasswordConnect
if shouldManageConnect() {
setupLog.Info("Automated Connect Management Enabled")
go func(ctx context.Context) {
connectStarted := false
for !connectStarted {
err := op.SetupConnect(ctx, 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
}
}
}(ctx)
} else {
setupLog.Info("Automated Connect Management Disabled")
}
// Setup update secrets task
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opClient, shouldAutoRestartDeployments())
done := make(chan bool)
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
go func(ctx context.Context) {
for {
select {
case <-done:
ticker.Stop()
return
case <-ticker.C:
err := updatedSecretsPoller.UpdateKubernetesSecretsTask(ctx)
if err != nil {
setupLog.Error(err, "error running update kubernetes secret task")
}
}
}
}(ctx)
if metricsCertWatcher != nil {
setupLog.Info("Adding metrics certificate watcher to manager")
if err := mgr.Add(metricsCertWatcher); err != nil {
setupLog.Error(err, "Unable to add metrics certificate watcher to manager")
os.Exit(1)
}
}
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(ctx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
// getWatchNamespace returns the Namespace the operator should be watching for changes
func getWatchNamespace() (string, error) {
// WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE
// which specifies the Namespace to watch.
// An empty value means the operator is running with cluster scope.
var watchNamespaceEnvVar = "WATCH_NAMESPACE"
ns, found := os.LookupEnv(watchNamespaceEnvVar)
if !found {
return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar)
}
return ns, nil
}
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
}
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
}
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
}

View File

@@ -1,302 +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 manageConnect = "MANAGE_CONNECT"
const restartDeploymentsEnvVariable = "AUTO_RESTART"
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 := os.Getenv(k8sutil.WatchNamespaceEnvVar)
deploymentNamespace, err := k8sutil.GetOperatorNamespace()
if err != nil {
log.Error(err, "Failed to get 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 1PasswordConnect
if shouldManageConnect() {
log.Info("Automated Connect Management Enabled")
go func() {
connectStarted := false
for connectStarted == false {
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{}) {
log.Error(err, "")
os.Exit(1)
}
if err == nil {
connectStarted = true
}
}
}()
} else {
log.Info("Automated Connect Management Disabled")
}
// 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, shouldAutoRestartDeployments())
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
}
func shouldManageConnect() bool {
shouldManageConnect, found := os.LookupEnv(manageConnect)
if found {
shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect))
if err != nil {
log.Error(err, "")
os.Exit(1)
}
return shouldManageConnectBool
}
return false
}
func shouldAutoRestartDeployments() bool {
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
if found {
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
if err != nil {
log.Error(err, "")
os.Exit(1)
}
return shouldAutoRestartDeploymentsBool
}
return false
}

View File

@@ -12,6 +12,10 @@ spec:
app: onepassword-connect
version: "1.0.0"
spec:
securityContext:
runAsNonRoot: true
fsGroup: 999
fsGroupChangePolicy: OnRootMismatch
volumes:
- name: shared-data
emptyDir: {}
@@ -29,12 +33,25 @@ spec:
volumeMounts:
- mountPath: /home/opuser/.op/data
name: shared-data
securityContext:
runAsUser: 0
runAsNonRoot: false
allowPrivilegeEscalation: false
capabilities:
drop: [ "ALL" ]
add: ["CHOWN", "FOWNER"]
containers:
- name: connect-api
image: 1password/connect-api:latest
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
allowPrivilegeEscalation: false
resources:
limits:
memory: "128Mi"
requests:
cpu: "0.2"
ports:
- containerPort: 8080
@@ -49,9 +66,15 @@ spec:
name: shared-data
- name: connect-sync
image: 1password/connect-sync:latest
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
allowPrivilegeEscalation: false
resources:
limits:
memory: "128Mi"
requests:
cpu: "0.2"
ports:
- containerPort: 8081

View File

@@ -9,7 +9,7 @@ spec:
ports:
- port: 8080
name: connect-api
nodePort: 31080
nodePort: 30080
- port: 8081
name: connect-sync
nodePort: 31081
nodePort: 30081

View File

@@ -0,0 +1,81 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.18.0
name: onepassworditems.onepassword.com
spec:
group: onepassword.com
names:
kind: OnePasswordItem
listKind: OnePasswordItemList
plural: onepassworditems
singular: onepassworditem
scope: Namespaced
versions:
- name: v1
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
properties:
conditions:
items:
properties:
lastTransitionTime:
description: Last time the condition transit from one status
to another.
format: date-time
type: string
message:
description: Human-readable message indicating details about
last transition.
type: string
status:
description: Status of the condition, one of True, False, Unknown.
type: string
type:
description: Type of job condition, Completed.
type: string
required:
- status
- type
type: object
type: array
required:
- conditions
type: object
type:
description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
type: string
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,17 @@
# 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.com_onepassworditems.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patches:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
#- path: patches/webhook_in_onepassworditems.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch
# [WEBHOOK] To enable webhook, uncomment the following section
# the following config is for teaching kustomize how to do kustomization for CRDs.
#configurations:
#- kustomizeconfig.yaml

View 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

View File

@@ -0,0 +1,234 @@
# Adds namespace to all resources.
# namespace: onepassword-connect-operator
# 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-connect-
# Labels to add to all resources and selectors.
#labels:
#- includeSelectors: true
# pairs:
# someName: someValue
resources:
- ../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
# [METRICS] Expose the controller manager metrics service.
- metrics_service.yaml
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
# be able to communicate with the Webhook Server.
#- ../network-policy
# Uncomment the patches line if you enable Metrics
patches:
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
# More info: https://book.kubebuilder.io/reference/metrics
- path: manager_metrics_patch.yaml
target:
kind: Deployment
# Uncomment the patches line if you enable Metrics and CertManager
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
# This patch will protect the metrics with certManager self-signed certs.
#- path: cert_metrics_manager_patch.yaml
# target:
# kind: Deployment
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- path: manager_webhook_patch.yaml
# target:
# kind: Deployment
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations
#replacements:
# - source: # Uncomment the following block to enable certificates for metrics
# kind: Service
# version: v1
# name: controller-manager-metrics-service
# fieldPath: metadata.name
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: metrics-certs
# fieldPaths:
# - spec.dnsNames.0
# - spec.dnsNames.1
# options:
# delimiter: '.'
# index: 0
# create: true
# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
# kind: ServiceMonitor
# group: monitoring.coreos.com
# version: v1
# name: controller-manager-metrics-monitor
# fieldPaths:
# - spec.endpoints.0.tlsConfig.serverName
# options:
# delimiter: '.'
# index: 0
# create: true
#
# - source:
# kind: Service
# version: v1
# name: controller-manager-metrics-service
# fieldPath: metadata.namespace
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: metrics-certs
# fieldPaths:
# - spec.dnsNames.0
# - spec.dnsNames.1
# options:
# delimiter: '.'
# index: 1
# create: true
# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
# kind: ServiceMonitor
# group: monitoring.coreos.com
# version: v1
# name: controller-manager-metrics-monitor
# fieldPaths:
# - spec.endpoints.0.tlsConfig.serverName
# options:
# delimiter: '.'
# index: 1
# create: true
#
# - source: # Uncomment the following block if you have any webhook
# kind: Service
# version: v1
# name: webhook-service
# fieldPath: .metadata.name # Name of the service
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPaths:
# - .spec.dnsNames.0
# - .spec.dnsNames.1
# options:
# delimiter: '.'
# index: 0
# create: true
# - source:
# kind: Service
# version: v1
# name: webhook-service
# fieldPath: .metadata.namespace # Namespace of the service
# targets:
# - select:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPaths:
# - .spec.dnsNames.0
# - .spec.dnsNames.1
# options:
# delimiter: '.'
# index: 1
# create: true
#
# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # This name should match the one in certificate.yaml
# fieldPath: .metadata.namespace # Namespace of the certificate CR
# targets:
# - select:
# kind: ValidatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - source:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.name
# targets:
# - select:
# kind: ValidatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 1
# create: true
#
# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.namespace # Namespace of the certificate CR
# targets:
# - select:
# kind: MutatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - source:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.name
# targets:
# - select:
# kind: MutatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 1
# create: true
#
# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.namespace # Namespace of the certificate CR
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
# +kubebuilder:scaffold:crdkustomizecainjectionns
# - source:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert
# fieldPath: .metadata.name
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
# +kubebuilder:scaffold:crdkustomizecainjectionname

View File

@@ -0,0 +1,4 @@
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
- op: add
path: /spec/template/spec/containers/0/args/0
value: --metrics-bind-address=:8443

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
labels:
control-plane: controller-manager
app.kubernetes.io/name: onepassword-operator
app.kubernetes.io/managed-by: kustomize
name: controller-manager-metrics-service
namespace: system
spec:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: 8443
selector:
control-plane: controller-manager

View File

@@ -0,0 +1,8 @@
resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: 1password/onepassword-operator
newTag: latest

134
config/manager/manager.yaml Normal file
View File

@@ -0,0 +1,134 @@
apiVersion: v1
kind: Namespace
metadata:
labels:
control-plane: onepassword-connect-operator
app.kubernetes.io/name: namespace
app.kubernetes.io/instance: system
app.kubernetes.io/component: manager
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: onepassword-connect-operator
namespace: system
labels:
control-plane: controller-manager
app.kubernetes.io/name: deployment
app.kubernetes.io/instance: controller-manager
app.kubernetes.io/component: manager
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
spec:
selector:
matchLabels:
name: onepassword-connect-operator
control-plane: onepassword-connect-operator
replicas: 1
template:
metadata:
annotations:
kubectl.kubernetes.io/default-container: manager
labels:
name: onepassword-connect-operator
control-plane: onepassword-connect-operator
spec:
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
# according to the platforms which are supported by your solution.
# It is considered best practice to support multiple architectures. You can
# build your manager image using the makefile target docker-buildx.
# affinity:
# nodeAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# nodeSelectorTerms:
# - matchExpressions:
# - key: kubernetes.io/arch
# operator: In
# values:
# - amd64
# - arm64
# - ppc64le
# - s390x
# - key: kubernetes.io/os
# operator: In
# values:
# - linux
securityContext:
runAsNonRoot: true
# TODO(user): For common cases that do not require escalating privileges
# it is recommended to ensure that all your Pods/Containers are restrictive.
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
# seccompProfile:
# type: RuntimeDefault
containers:
- command:
- /manager
args:
- --leader-elect
- --health-probe-bind-address=:8081
image: 1password/onepassword-operator:latest
imagePullPolicy: Never
name: manager
env:
- name: OPERATOR_NAME
value: "onepassword-connect-operator"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: WATCH_NAMESPACE
value: "default"
- name: POLLING_INTERVAL
value: "10"
- name: AUTO_RESTART
value: "false"
- name: OP_CONNECT_HOST
value: "http://onepassword-connect:8080"
- name: OP_CONNECT_TOKEN
valueFrom:
secretKeyRef:
name: onepassword-token
key: token
- name: MANAGE_CONNECT
value: "true"
# Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN, OP_CONNECT_HOST and MANAGE_CONNECT env vars.
# - name: OP_SERVICE_ACCOUNT_TOKEN
# valueFrom:
# secretKeyRef:
# name: onepassword-service-account-token
# key: token
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
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: 512Mi
requests:
cpu: 100m
memory: 128Mi
serviceAccountName: onepassword-connect-operator
terminationGracePeriodSeconds: 10

View File

@@ -0,0 +1,28 @@
# These resources constitute the fully configured set of manifests
# used to generate the 'manifests/' directory in a bundle.
resources:
- bases/onepassword-operator.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/0/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

View File

@@ -0,0 +1,26 @@
# This NetworkPolicy allows ingress traffic
# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those
# namespaces are able to gathering data from the metrics endpoint.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
labels:
app.kubernetes.io/name: onepassword-operator
app.kubernetes.io/managed-by: kustomize
name: allow-metrics-traffic
namespace: system
spec:
podSelector:
matchLabels:
control-plane: controller-manager
policyTypes:
- Ingress
ingress:
# This allows ingress traffic from any namespace with the label metrics: enabled
- from:
- namespaceSelector:
matchLabels:
metrics: enabled # Only from namespaces with this label
ports:
- port: 8443
protocol: TCP

View File

@@ -0,0 +1,2 @@
resources:
- allow-metrics-traffic.yaml

View File

@@ -0,0 +1,11 @@
resources:
- monitor.yaml
# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
# to securely reference certificates created and managed by cert-manager.
# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
# to mount the "metrics-server-cert" secret in the Manager Deployment.
#patches:
# - path: monitor_tls_patch.yaml
# target:
# kind: ServiceMonitor

View File

@@ -0,0 +1,37 @@
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
name: onepassword-connect-operator
control-plane: onepassword-connect-operator
app.kubernetes.io/name: onepassword-operator
app.kubernetes.io/instance: controller-manager-metrics-monitor
app.kubernetes.io/component: metrics
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: onepassword-connect-operator-metrics-monitor
namespace: system
spec:
endpoints:
- path: /metrics
port: https # Ensure this is the name of the port that exposes HTTPS metrics
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig:
# TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
# certificate verification. This poses a significant security risk by making the system vulnerable to
# man-in-the-middle attacks, where an attacker could intercept and manipulate the communication between
# Prometheus and the monitored services. This could lead to unauthorized access to sensitive metrics data,
# compromising the integrity and confidentiality of the information.
# Please use the following options for secure configurations:
# caFile: /etc/metrics-certs/ca.crt
# certFile: /etc/metrics-certs/tls.crt
# keyFile: /etc/metrics-certs/tls.key
insecureSkipVerify: true
selector:
matchLabels:
name: onepassword-connect-operator
control-plane: onepassword-connect-operator
app.kubernetes.io/name: onepassword-operator

View File

@@ -0,0 +1,19 @@
# Patch for Prometheus ServiceMonitor to enable secure TLS configuration
# using certificates managed by cert-manager
- op: replace
path: /spec/endpoints/0/tlsConfig
value:
# SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc
insecureSkipVerify: false
ca:
secret:
name: metrics-server-cert
key: ca.crt
cert:
secret:
name: metrics-server-cert
key: tls.crt
keySecret:
name: metrics-server-cert
key: tls.key

View File

@@ -0,0 +1,27 @@
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
# The following RBAC configurations are used to protect
# the metrics endpoint with authn/authz. These configurations
# ensure that only authorized users and service accounts
# can access the metrics endpoint. Comment the following
# permissions if you want to disable this protection.
# More info: https://book.kubebuilder.io/reference/metrics.html
- metrics_auth_role.yaml
- metrics_auth_role_binding.yaml
- metrics_reader_role.yaml
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
# default, aiding admins in cluster management. Those roles are
# not used by the {{ .ProjectName }} itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
- onepassworditem_admin_role.yaml
- onepassworditem_editor_role.yaml
- onepassworditem_viewer_role.yaml

View File

@@ -0,0 +1,44 @@
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app.kubernetes.io/name: role
app.kubernetes.io/instance: leader-election-role
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
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

View File

@@ -0,0 +1,19 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app.kubernetes.io/name: rolebinding
app.kubernetes.io/instance: leader-election-rolebinding
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: leader-election-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: leader-election-role
subjects:
- kind: ServiceAccount
name: onepassword-connect-operator
namespace: system

View File

@@ -0,0 +1,17 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-auth-role
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create

View File

@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: metrics-auth-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: metrics-auth-role
subjects:
- kind: ServiceAccount
name: controller-manager
namespace: system

View File

@@ -0,0 +1,9 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: metrics-reader
rules:
- nonResourceURLs:
- "/metrics"
verbs:
- get

View File

@@ -0,0 +1,31 @@
# This rule is not used by the project onepassword-operator itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over onepassword.com.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: clusterrole
app.kubernetes.io/instance: onepassworditem-admin-role
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: onepassworditem-admin-role
rules:
- apiGroups:
- onepassword.com
resources:
- onepassworditems
verbs:
- '*'
- apiGroups:
- onepassword.com
resources:
- onepassworditems/status
verbs:
- get

View File

@@ -0,0 +1,37 @@
# This rule is not used by the project onepassword-operator itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the onepassword.com.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: clusterrole
app.kubernetes.io/instance: onepassworditem-editor-role
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: onepassworditem-editor-role
rules:
- apiGroups:
- onepassword.com
resources:
- onepassworditems
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- onepassword.com
resources:
- onepassworditems/status
verbs:
- get

View File

@@ -0,0 +1,33 @@
# This rule is not used by the project onepassword-operator itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to onepassword.com resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: clusterrole
app.kubernetes.io/instance: onepassworditem-viewer-role
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: onepassworditem-viewer-role
rules:
- apiGroups:
- onepassword.com
resources:
- onepassworditems
verbs:
- get
- list
- watch
- apiGroups:
- onepassword.com
resources:
- onepassworditems/status
verbs:
- get

View File

@@ -1,40 +1,21 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: onepassword-connect-operator
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: onepassword-connect-operator-default
namespace: default
subjects:
- kind: ServiceAccount
name: onepassword-connect-operator
namespace: default
roleRef:
kind: ClusterRole
name: onepassword-connect-operator
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: onepassword-connect-operator
name: manager-role
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- events
- namespaces
- persistentvolumeclaims
- pods
- secrets
- services
- services/finalizers
- endpoints
- persistentvolumeclaims
- events
- configmaps
- secrets
- namespaces
verbs:
- create
- delete
@@ -46,8 +27,8 @@ rules:
- apiGroups:
- apps
resources:
- deployments
- daemonsets
- deployments
- replicasets
- statefulsets
verbs:
@@ -59,12 +40,11 @@ rules:
- update
- watch
- apiGroups:
- monitoring.coreos.com
- apps
resources:
- servicemonitors
- deployments/finalizers
verbs:
- get
- create
- update
- apiGroups:
- apps
resourceNames:
@@ -73,23 +53,35 @@ rules:
- deployments/finalizers
verbs:
- update
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- apps
resources:
- replicasets
- deployments
- deployments/status
verbs:
- get
- patch
- update
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- create
- get
- list
- update
- apiGroups:
- monitoring.coreos.com
resources:
- servicemonitors
verbs:
- create
- get
- apiGroups:
- onepassword.com
resources:
- '*'
- onepassworditems
verbs:
- create
- delete
@@ -98,3 +90,17 @@ rules:
- patch
- update
- watch
- apiGroups:
- onepassword.com
resources:
- onepassworditems/finalizers
verbs:
- update
- apiGroups:
- onepassword.com
resources:
- onepassworditems/status
verbs:
- get
- patch
- update

View File

@@ -0,0 +1,19 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app.kubernetes.io/name: clusterrolebinding
app.kubernetes.io/instance: manager-rolebinding
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
subjects:
- kind: ServiceAccount
name: onepassword-connect-operator
namespace: system

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/name: serviceaccount
app.kubernetes.io/instance: controller-manager-sa
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: onepassword-connect-operator
namespace: system

View 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

View File

@@ -0,0 +1,12 @@
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
labels:
app.kubernetes.io/name: onepassworditem
app.kubernetes.io/instance: onepassworditem-sample
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/created-by: onepassword-connect-operator
name: onepassworditem-sample
spec:
itemPath: "vaults/<vault_id>/items/<item_id>"

View File

@@ -0,0 +1,7 @@
apiVersion: scorecard.operatorframework.io/v1alpha3
kind: Configuration
metadata:
name: config
stages:
- parallel: true
tests: []

View 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

View 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.33.0
labels:
suite: basic
test: basic-check-spec-test

View 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.33.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.33.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.33.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.33.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.33.0
labels:
suite: olm
test: olm-status-descriptors-test

View File

@@ -1,42 +0,0 @@
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: object

View File

@@ -1,6 +0,0 @@
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: example
spec:
itemPath: "vaults/<vault_id>/items/<item_id>"

View File

@@ -1,39 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: onepassword-connect-operator
spec:
replicas: 1
selector:
matchLabels:
name: onepassword-connect-operator
template:
metadata:
labels:
name: onepassword-connect-operator
spec:
serviceAccountName: onepassword-connect-operator
containers:
- name: onepassword-connect-operator
image: 1password/onepassword-operator
command: ["/manager"]
env:
- name: WATCH_NAMESPACE
value: "default"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: "onepassword-connect-operator"
- name: OP_CONNECT_HOST
value: "http://onepassword-connect:8080"
- name: POLLING_INTERVAL
value: "10"
- name: OP_CONNECT_TOKEN
valueFrom:
secretKeyRef:
name: onepassword-token
key: token
- name: AUTO_RESTART
value: "false"

View File

@@ -1,39 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: onepassword-connect-operator
spec:
replicas: 1
selector:
matchLabels:
name: onepassword-connect-operator
template:
metadata:
labels:
name: onepassword-connect-operator
spec:
serviceAccountName: onepassword-connect-operator
containers:
- name: onepassword-connect-operator
image: 1password/onepassword-operator
command: ["/manager"]
env:
- name: WATCH_NAMESPACE
value: "default,development"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: "onepassword-connect-operator"
- name: OP_CONNECT_HOST
value: "http://onepassword-connect:8080"
- name: POLLING_INTERVAL
value: "10"
- name: OP_CONNECT_TOKEN
valueFrom:
secretKeyRef:
name: onepassword-token
key: token
- name: AUTO_RESTART
value: "false"

View File

@@ -1,114 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: onepassword-connect-operator
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: onepassword-connect-operator-default
namespace: default
subjects:
- kind: ServiceAccount
name: onepassword-connect-operator
namespace: default
roleRef:
kind: ClusterRole
name: onepassword-connect-operator
apiGroup: rbac.authorization.k8s.io
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: onepassword-connect-operator-development
namespace: development
subjects:
- kind: ServiceAccount
name: onepassword-connect-operator
namespace: default
roleRef:
kind: ClusterRole
name: onepassword-connect-operator
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: onepassword-connect-operator
rules:
- apiGroups:
- ""
resources:
- pods
- services
- services/finalizers
- endpoints
- persistentvolumeclaims
- events
- configmaps
- secrets
- namespaces
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
- deployments
- daemonsets
- replicasets
- statefulsets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- monitoring.coreos.com
resources:
- servicemonitors
verbs:
- get
- create
- apiGroups:
- apps
resourceNames:
- onepassword-connect-operator
resources:
- deployments/finalizers
verbs:
- update
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- apps
resources:
- replicasets
- deployments
verbs:
- get
- apiGroups:
- onepassword.com
resources:
- '*'
verbs:
- create
- delete
- get
- list
- patch
- update
- watch

27
docs/testing.md Normal file
View File

@@ -0,0 +1,27 @@
# Testing
## Unit tests
**When**: Pure Go logic, no Kubernetes apiserver or network.
**Where**: `internal/...`, `pkg/...`
**Add files in**: `*_test.go` next to the code.
**Run**: `make test`
## Integration tests (envtest)
**When**: Controller/reconciler behavior against a mocked kubernetes cluster.
**Where**: `internal/controller/...`
**Framework**: controller-runtimes `envtest`.
**Run**: `make test`
## E2E tests (kind)
**When**: Full cluster behavior (CRDs, operator image, Connect/SA flows).
**Where**: `test/e2e/...`
**Framework**: Ginkgo + `pkg/testhelper`.
**Local prep**:
1. [Install `kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installing-with-a-package-manager) to spin up local Kubernetes cluster.
2. `export OP_CONNECT_TOKEN=<token>`
3. `export OP_SERVICE_ACCOUNT_TOKEN=<token>`
4. `make test-e2e`
5. Put `1password-credentials.json` into project root.
**Run**: `make test-e2e`

123
go.mod
View File

@@ -1,21 +1,116 @@
module github.com/1Password/onepassword-operator
go 1.13
go 1.24.0
toolchain go1.24.5
require (
github.com/1Password/connect-sdk-go v1.0.1
github.com/operator-framework/operator-sdk v0.19.0
github.com/prometheus/common v0.14.0 // indirect
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.6.1
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
github.com/1Password/connect-sdk-go v1.5.3
github.com/1password/onepassword-sdk-go v0.3.1
github.com/go-logr/logr v1.4.2
github.com/onsi/ginkgo/v2 v2.22.0
github.com/onsi/gomega v1.36.1
github.com/stretchr/testify v1.10.0
k8s.io/api v0.33.0
k8s.io/apiextensions-apiserver v0.33.0
k8s.io/apimachinery v0.33.0
k8s.io/client-go v0.33.0
k8s.io/kubectl v0.29.0
sigs.k8s.io/controller-runtime v0.21.0
)
replace (
github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM
k8s.io/client-go => k8s.io/client-go v0.18.2 // Required by prometheus-operator
require (
cel.dev/expr v0.19.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/extism/go-sdk v1.7.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.23.2 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.33.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.68.1 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiserver v0.33.0 // indirect
k8s.io/component-base v0.33.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

1595
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
/*
MIT License
Copyright (c) 2016 json-iterator
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +20,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

View File

@@ -0,0 +1,223 @@
/*
MIT License
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package controller
import (
"context"
"fmt"
"regexp"
"strings"
"time"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/utils"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var logDeployment = logf.Log.WithName("controller_deployment")
// DeploymentReconciler reconciles a Deployment object
type DeploymentReconciler struct {
client.Client
Scheme *runtime.Scheme
OpClient opclient.Client
OpAnnotationRegExp *regexp.Regexp
}
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments/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/pkg/reconcile
func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
reqLogger := logDeployment.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name)
reqLogger.V(logs.DebugLevel).Info("Reconciling Deployment")
deployment := &appsv1.Deployment{}
err := r.Get(ctx, req.NamespacedName, deployment)
if err != nil {
if errors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return ctrl.Result{}, err
}
annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.OpAnnotationRegExp)
if !annotationsFound {
reqLogger.V(logs.DebugLevel).Info("No 1Password Annotations found")
return ctrl.Result{}, nil
}
// If the deployment is not being deleted
if deployment.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(deployment.Finalizers, finalizer) {
deployment.Finalizers = append(deployment.Finalizers, finalizer)
if err = r.Update(ctx, deployment); err != nil {
return reconcile.Result{}, err
}
}
// Handles creation or updating secrets for deployment if needed
if err = r.handleApplyingDeployment(ctx, deployment, deployment.Namespace, annotations, req); err != nil {
if strings.Contains(err.Error(), "rate limit") {
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
} else {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// The deployment has been marked for deletion. If the one password
// finalizer is found there are cleanup tasks to perform
if utils.ContainsString(deployment.Finalizers, finalizer) {
secretName := annotations[op.NameAnnotation]
if err = r.cleanupKubernetesSecretForDeployment(ctx, secretName, deployment); err != nil {
return ctrl.Result{}, err
}
// Remove the finalizer from the deployment so deletion of deployment can be completed
if err = r.removeOnePasswordFinalizerFromDeployment(ctx, deployment); err != nil {
return reconcile.Result{}, err
}
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1.Deployment{}).
Named("onepassword-deployment").
Complete(r)
}
func (r *DeploymentReconciler) cleanupKubernetesSecretForDeployment(ctx context.Context, secretName string, deletedDeployment *appsv1.Deployment) error {
kubernetesSecret := &corev1.Secret{}
kubernetesSecret.Name = secretName
kubernetesSecret.Namespace = deletedDeployment.Namespace
if len(secretName) == 0 {
return nil
}
updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret}
multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(ctx, updatedSecrets, *deletedDeployment)
if err != nil {
return err
}
// Only delete the associated kubernetes secret if it is not being used by other deployments
if !multipleDeploymentsUsingSecret {
if err = r.Delete(ctx, kubernetesSecret); err != nil {
if !errors.IsNotFound(err) {
return err
}
}
}
return nil
}
func (r *DeploymentReconciler) areMultipleDeploymentsUsingSecret(ctx context.Context, updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) {
deployments := &appsv1.DeploymentList{}
opts := []client.ListOption{
client.InNamespace(deletedDeployment.Namespace),
}
err := r.List(ctx, deployments, opts...)
if err != nil {
logDeployment.Error(err, "Failed to list kubernetes deployments")
return false, err
}
for i := 0; i < len(deployments.Items); i++ {
if deployments.Items[i].Name != deletedDeployment.Name {
if op.IsDeploymentUsingSecrets(&deployments.Items[i], updatedSecrets) {
return true, nil
}
}
}
return false, nil
}
func (r *DeploymentReconciler) removeOnePasswordFinalizerFromDeployment(ctx context.Context, deployment *appsv1.Deployment) error {
deployment.Finalizers = utils.RemoveString(deployment.Finalizers, finalizer)
return r.Update(ctx, deployment)
}
func (r *DeploymentReconciler) handleApplyingDeployment(ctx context.Context, deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error {
reqLog := logDeployment.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
secretName := annotations[op.NameAnnotation]
secretLabels := map[string]string(nil)
secretType := string(corev1.SecretTypeOpaque)
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.")
return nil
}
item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, annotations[op.ItemPathAnnotation])
if err != nil {
return fmt.Errorf("failed to retrieve item: %w", err)
}
// Create owner reference.
gvk, err := apiutil.GVKForObject(deployment, r.Scheme)
if err != nil {
return fmt.Errorf("could not to retrieve group version kind: %w", err)
}
ownerRef := &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: deployment.GetName(),
UID: deployment.GetUID(),
}
return kubeSecrets.CreateKubernetesSecretFromItem(ctx, r.Client, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef)
}

View File

@@ -0,0 +1,390 @@
package controller
import (
"context"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
)
const (
deploymentKind = "Deployment"
deploymentAPIVersion = "v1"
deploymentName = "test-deployment"
)
var _ = Describe("Deployment controller", func() {
ctx := context.Background()
var deploymentKey types.NamespacedName
var secretKey types.NamespacedName
var deploymentResource *appsv1.Deployment
createdSecret := &v1.Secret{}
makeDeployment := func() {
deploymentKey = types.NamespacedName{
Name: deploymentName,
Namespace: namespace,
}
secretKey = types.NamespacedName{
Name: item1.Name,
Namespace: namespace,
}
By("Deploying a pod with proper annotations successfully")
deploymentResource = &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: deploymentKey.Name,
Namespace: deploymentKey.Namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: item1.Path,
op.NameAnnotation: item1.Name,
},
},
Spec: appsv1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": deploymentName},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: deploymentName,
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
ImagePullPolicy: "IfNotPresent",
},
},
},
},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": deploymentName},
},
},
}
Expect(k8sClient.Create(ctx, deploymentResource)).Should(Succeed())
By("Creating the K8s secret successfully")
time.Sleep(time.Millisecond * 100)
Eventually(func() bool {
err := k8sClient.Get(ctx, secretKey, createdSecret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(createdSecret.Data).Should(Equal(item1.SecretData))
}
cleanK8sResources := func() {
// failed test runs that don't clean up leave resources behind.
err := k8sClient.DeleteAllOf(ctx, &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
err = k8sClient.DeleteAllOf(ctx, &v1.Secret{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
err = k8sClient.DeleteAllOf(ctx, &appsv1.Deployment{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
}
mockGetItemFunc := func() {
// mock GetItemByID to return test item 'item1'
mockGetItemByIDFunc.Return(item1.ToModel(), nil)
}
BeforeEach(func() {
cleanK8sResources()
mockGetItemFunc()
time.Sleep(time.Second) // TODO: can we achieve that with ginkgo?
makeDeployment()
})
Context("Deployment with secrets from 1Password", func() {
It("Should delete secret if deployment is deleted", func() {
By("Deleting the pod")
Eventually(func() error {
f := &appsv1.Deployment{}
err := k8sClient.Get(ctx, deploymentKey, f)
if err != nil {
return err
}
return k8sClient.Delete(ctx, f)
}, timeout, interval).Should(Succeed())
Eventually(func() error {
f := &appsv1.Deployment{}
return k8sClient.Get(ctx, deploymentKey, f)
}, timeout, interval).ShouldNot(Succeed())
Eventually(func() error {
f := &v1.Secret{}
return k8sClient.Get(ctx, secretKey, f)
}, timeout, interval).ShouldNot(Succeed())
})
It("Should update existing K8s Secret using deployment", func() {
By("Updating secret")
// mock GetItemByID to return test item 'item2'
mockGetItemByIDFunc.Return(item2.ToModel(), nil)
Eventually(func() error {
updatedDeployment := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: deploymentKey.Name,
Namespace: deploymentKey.Namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: item2.Path,
op.NameAnnotation: item1.Name,
},
},
Spec: appsv1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": deploymentName},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: deploymentName,
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
ImagePullPolicy: "IfNotPresent",
},
},
},
},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": deploymentName},
},
},
}
err := k8sClient.Update(ctx, updatedDeployment)
if err != nil {
return err
}
return nil
}, timeout, interval).Should(Succeed())
// TODO: can we achieve the same without sleep?
time.Sleep(time.Millisecond * 10)
By("Reading updated K8s secret")
updatedSecret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, secretKey, updatedSecret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(updatedSecret.Data).Should(Equal(item2.SecretData))
})
It("Should not update secret if Annotations have not changed", func() {
By("Updating secret without changing annotations")
Eventually(func() error {
updatedDeployment := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: deploymentKey.Name,
Namespace: deploymentKey.Namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: item1.Path,
op.NameAnnotation: item1.Name,
},
},
Spec: appsv1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": deploymentName},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: deploymentName,
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
ImagePullPolicy: "IfNotPresent",
},
},
},
},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": deploymentName},
},
},
}
err := k8sClient.Update(ctx, updatedDeployment)
if err != nil {
return err
}
return nil
}, timeout, interval).Should(Succeed())
// TODO: can we achieve the same without sleep?
time.Sleep(time.Millisecond * 10)
By("Reading updated K8s secret")
updatedSecret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, secretKey, updatedSecret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(updatedSecret.Data).Should(Equal(item1.SecretData))
})
It("Should not delete secret created via deployment if it's used in another container", func() {
By("Creating another POD with created secret")
anotherDeploymentKey := types.NamespacedName{
Name: "other-deployment",
Namespace: namespace,
}
Eventually(func() error {
anotherDeployment := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: anotherDeploymentKey.Name,
Namespace: anotherDeploymentKey.Namespace,
},
Spec: appsv1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": anotherDeploymentKey.Name},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: anotherDeploymentKey.Name,
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
ImagePullPolicy: "IfNotPresent",
Env: []v1.EnvVar{
{
Name: anotherDeploymentKey.Name,
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: secretKey.Name,
},
Key: "password",
},
},
},
},
},
},
},
},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": anotherDeploymentKey.Name},
},
},
}
err := k8sClient.Create(ctx, anotherDeployment)
if err != nil {
return err
}
return nil
}, timeout, interval).Should(Succeed())
By("Deleting the pod")
Eventually(func() error {
f := &appsv1.Deployment{}
err := k8sClient.Get(ctx, deploymentKey, f)
if err != nil {
return err
}
return k8sClient.Delete(ctx, f)
}, timeout, interval).Should(Succeed())
Eventually(func() error {
f := &v1.Secret{}
return k8sClient.Get(ctx, secretKey, f)
}, timeout, interval).Should(Succeed())
})
It("Should not delete secret created via deployment if it's used in another volume", func() {
By("Creating another POD with created secret")
anotherDeploymentKey := types.NamespacedName{
Name: "other-deployment",
Namespace: namespace,
}
Eventually(func() error {
anotherDeployment := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: anotherDeploymentKey.Name,
Namespace: anotherDeploymentKey.Namespace,
},
Spec: appsv1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": anotherDeploymentKey.Name},
},
Spec: v1.PodSpec{
Volumes: []v1.Volume{
{
Name: anotherDeploymentKey.Name,
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{
SecretName: secretKey.Name,
},
},
},
},
Containers: []v1.Container{
{
Name: anotherDeploymentKey.Name,
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
ImagePullPolicy: "IfNotPresent",
},
},
},
},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": anotherDeploymentKey.Name},
},
},
}
err := k8sClient.Create(ctx, anotherDeployment)
if err != nil {
return err
}
return nil
}, timeout, interval).Should(Succeed())
By("Deleting the pod")
Eventually(func() error {
f := &appsv1.Deployment{}
err := k8sClient.Get(ctx, deploymentKey, f)
if err != nil {
return err
}
return k8sClient.Delete(ctx, f)
}, timeout, interval).Should(Succeed())
Eventually(func() error {
f := &v1.Secret{}
return k8sClient.Get(ctx, secretKey, f)
}, timeout, interval).Should(Succeed())
})
})
})

View File

@@ -0,0 +1,216 @@
/*
MIT License
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package controller
import (
"context"
"fmt"
"strings"
"time"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
)
var logOnePasswordItem = logf.Log.WithName("controller_onepassworditem")
var finalizer = "onepassword.com/finalizer.secret"
// OnePasswordItemReconciler reconciles a OnePasswordItem object
type OnePasswordItemReconciler struct {
client.Client
Scheme *runtime.Scheme
OpClient opclient.Client
}
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=pods,verbs=get
// +kubebuilder:rbac:groups="",resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets;namespaces,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=daemonsets;deployments;replicasets;statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=replicasets;deployments,verbs=get
// +kubebuilder:rbac:groups=apps,resourceNames=onepassword-connect-operator,resources=deployments/finalizers,verbs=update
// +kubebuilder:rbac:groups=onepassword.com,resources=*,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;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/pkg/reconcile
func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
reqLogger := logOnePasswordItem.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name)
reqLogger.V(logs.DebugLevel).Info("Reconciling OnePasswordItem")
onepassworditem := &onepasswordv1.OnePasswordItem{}
err := r.Get(ctx, req.NamespacedName, onepassworditem)
if err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// If the deployment is not being deleted
if onepassworditem.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.Finalizers, finalizer) {
onepassworditem.Finalizers = append(onepassworditem.Finalizers, finalizer)
if err = r.Update(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err
}
}
// Handles creation or updating secrets for deployment if needed
err = r.handleOnePasswordItem(ctx, onepassworditem, req)
if err != nil {
if strings.Contains(err.Error(), "rate limit") {
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
}
}
if updateStatusErr := r.updateStatus(ctx, onepassworditem, err); updateStatusErr != nil {
return ctrl.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr)
}
return ctrl.Result{}, err
}
// If one password finalizer exists then we must cleanup associated secrets
if utils.ContainsString(onepassworditem.Finalizers, finalizer) {
// Delete associated kubernetes secret
if err = r.cleanupKubernetesSecret(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err
}
// Remove finalizer now that cleanup is complete
if err = r.removeOnePasswordFinalizerFromOnePasswordItem(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&onepasswordv1.OnePasswordItem{}).
Named("onepassworditem").
Complete(r)
}
func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
kubernetesSecret := &corev1.Secret{}
kubernetesSecret.Name = onePasswordItem.Name
kubernetesSecret.Namespace = onePasswordItem.Namespace
if err := r.Delete(ctx, kubernetesSecret); err != nil {
if !errors.IsNotFound(err) {
return err
}
}
return nil
}
func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
onePasswordItem.Finalizers = utils.RemoveString(onePasswordItem.Finalizers, finalizer)
return r.Update(ctx, onePasswordItem)
}
func (r *OnePasswordItemReconciler) handleOnePasswordItem(ctx context.Context, resource *onepasswordv1.OnePasswordItem, _ ctrl.Request) error {
secretName := resource.GetName()
labels := resource.Labels
secretType := resource.Type
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, resource.Spec.ItemPath)
if err != nil {
return fmt.Errorf("failed to retrieve item: %w", err)
}
// Create owner reference.
gvk, err := apiutil.GVKForObject(resource, r.Scheme)
if err != nil {
return fmt.Errorf("could not to retrieve group version kind: %w", err)
}
ownerRef := &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: resource.GetName(),
UID: resource.GetUID(),
}
return kubeSecrets.CreateKubernetesSecretFromItem(ctx, r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef)
}
func (r *OnePasswordItemReconciler) updateStatus(ctx context.Context, resource *onepasswordv1.OnePasswordItem, err error) error {
existingCondition := findCondition(resource.Status.Conditions, onepasswordv1.OnePasswordItemReady)
updatedCondition := existingCondition
if err != nil {
updatedCondition.Message = err.Error()
updatedCondition.Status = metav1.ConditionFalse
} else {
updatedCondition.Message = ""
updatedCondition.Status = metav1.ConditionTrue
}
if existingCondition.Status != updatedCondition.Status {
updatedCondition.LastTransitionTime = metav1.Now()
}
resource.Status.Conditions = []onepasswordv1.OnePasswordItemCondition{updatedCondition}
return r.Status().Update(ctx, resource)
}
func findCondition(conditions []onepasswordv1.OnePasswordItemCondition, t onepasswordv1.OnePasswordItemConditionType) onepasswordv1.OnePasswordItemCondition {
for _, c := range conditions {
if c.Type == t {
return c
}
}
return onepasswordv1.OnePasswordItemCondition{
Type: t,
Status: metav1.ConditionUnknown,
}
}

View File

@@ -0,0 +1,426 @@
package controller
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
const (
firstHost = "http://localhost:8080"
awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
iceCream = "freezing blue 20%"
)
var _ = Describe("OnePasswordItem controller", func() {
BeforeEach(func() {
// failed test runs that don't clean up leave resources behind.
err := k8sClient.DeleteAllOf(context.Background(), &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
item := item1.ToModel()
mockGetItemByIDFunc.Return(item, nil)
})
Context("Happy path", func() {
It("Should handle 1Password Item and secret correctly", func() {
ctx := context.Background()
spec := onepasswordv1.OnePasswordItemSpec{
ItemPath: item1.Path,
}
key := types.NamespacedName{
Name: "sample-item",
Namespace: namespace,
}
toCreate := &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: spec,
}
By("Creating a new OnePasswordItem successfully")
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
created := &onepasswordv1.OnePasswordItem{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, created)
return err == nil
}, timeout, interval).Should(BeTrue())
By("Creating the K8s secret successfully")
createdSecret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, createdSecret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(createdSecret.Data).Should(Equal(item1.SecretData))
By("Updating existing secret successfully")
newData := map[string]string{
"username": "newUser1234",
"password": "##newPassword##",
"extraField": "dev",
}
newDataByte := map[string][]byte{
"username": []byte("newUser1234"),
"password": []byte("##newPassword##"),
"extraField": []byte("dev"),
}
item := item2.ToModel()
for k, v := range newData {
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
}
mockGetItemByIDFunc.Return(item, nil)
_, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})
Expect(err).ToNot(HaveOccurred())
updatedSecret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, updatedSecret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(updatedSecret.Data).Should(Equal(newDataByte))
By("Deleting the OnePasswordItem successfully")
Eventually(func() error {
f := &onepasswordv1.OnePasswordItem{}
err := k8sClient.Get(ctx, key, f)
if err != nil {
return err
}
return k8sClient.Delete(ctx, f)
}, timeout, interval).Should(Succeed())
Eventually(func() error {
f := &onepasswordv1.OnePasswordItem{}
return k8sClient.Get(ctx, key, f)
}, timeout, interval).ShouldNot(Succeed())
Eventually(func() error {
f := &v1.Secret{}
return k8sClient.Get(ctx, key, f)
}, timeout, interval).ShouldNot(Succeed())
})
It("Should handle 1Password Item with fields and sections that have invalid K8s labels correctly", func() {
ctx := context.Background()
spec := onepasswordv1.OnePasswordItemSpec{
ItemPath: item1.Path,
}
key := types.NamespacedName{
Name: "my-secret-it3m",
Namespace: namespace,
}
toCreate := &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: spec,
}
testData := map[string]string{
"username": username,
"password": password,
"first host": firstHost,
"AWS Access Key": awsKey,
"😄 ice-cream type": iceCream,
}
expectedData := map[string][]byte{
"username": []byte(username),
"password": []byte(password),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"ice-cream-type": []byte(iceCream),
}
item := item2.ToModel()
for k, v := range testData {
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
}
mockGetItemByIDFunc.Return(item, nil)
By("Creating a new OnePasswordItem successfully")
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
created := &onepasswordv1.OnePasswordItem{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, created)
return err == nil
}, timeout, interval).Should(BeTrue())
By("Creating the K8s secret successfully")
createdSecret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, createdSecret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(createdSecret.Data).Should(Equal(expectedData))
By("Deleting the OnePasswordItem successfully")
Eventually(func() error {
f := &onepasswordv1.OnePasswordItem{}
err := k8sClient.Get(ctx, key, f)
if err != nil {
return err
}
return k8sClient.Delete(ctx, f)
}, timeout, interval).Should(Succeed())
Eventually(func() error {
f := &onepasswordv1.OnePasswordItem{}
return k8sClient.Get(ctx, key, f)
}, timeout, interval).ShouldNot(Succeed())
Eventually(func() error {
f := &v1.Secret{}
return k8sClient.Get(ctx, key, f)
}, timeout, interval).ShouldNot(Succeed())
})
It("Should not update K8s secret if OnePasswordItem Version or VaultPath has not changed", func() {
ctx := context.Background()
spec := onepasswordv1.OnePasswordItemSpec{
ItemPath: item1.Path,
}
key := types.NamespacedName{
Name: "item-not-updated",
Namespace: namespace,
}
toCreate := &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: spec,
}
By("Creating a new OnePasswordItem successfully")
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
item := &onepasswordv1.OnePasswordItem{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, item)
return err == nil
}, timeout, interval).Should(BeTrue())
By("Creating the K8s secret successfully")
createdSecret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, createdSecret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(createdSecret.Data).Should(Equal(item1.SecretData))
By("Updating OnePasswordItem type")
Eventually(func() bool {
err1 := k8sClient.Get(ctx, key, item)
if err1 != nil {
return false
}
item.Type = string(v1.SecretTypeOpaque)
err := k8sClient.Update(ctx, item)
return err == nil
}, timeout, interval).Should(BeTrue())
By("Reading K8s secret")
secret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, secret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(secret.Data).Should(Equal(item1.SecretData))
})
It("Should create custom K8s Secret type using OnePasswordItem", func() {
const customType = "CustomType"
ctx := context.Background()
spec := onepasswordv1.OnePasswordItemSpec{
ItemPath: item1.Path,
}
key := types.NamespacedName{
Name: "item-custom-secret-type",
Namespace: namespace,
}
toCreate := &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: spec,
Type: customType,
}
By("Creating a new OnePasswordItem successfully")
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
By("Reading K8s secret")
secret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, secret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(secret.Type).Should(Equal(v1.SecretType(customType)))
})
It("Should handle 1Password Item with a file and populate secret correctly", func() {
ctx := context.Background()
spec := onepasswordv1.OnePasswordItemSpec{
ItemPath: item1.Path,
}
key := types.NamespacedName{
Name: "item-with-file",
Namespace: namespace,
}
toCreate := &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: spec,
}
fileContent := []byte("dummy-cert-content")
item := item1.ToModel()
item.Files = []model.File{
{
ID: "file-id-123",
Name: "server.crt",
ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/file-id-123/content", item.VaultID, item.ID),
},
}
item.Files[0].SetContent(fileContent)
mockGetItemByIDFunc.Return(item, nil)
mockGetItemByIDFunc.On("GetFileContent", item.VaultID, item.ID, "file-id-123").Return(fileContent, nil)
By("Creating a new OnePasswordItem with file successfully")
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
createdSecret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, createdSecret)
return err == nil
}, timeout, interval).Should(BeTrue())
Expect(createdSecret.Data).Should(HaveKeyWithValue("server.crt", fileContent))
})
})
Context("Unhappy path", func() {
It("Should throw an error if K8s Secret type is changed", func() {
ctx := context.Background()
spec := onepasswordv1.OnePasswordItemSpec{
ItemPath: item1.Path,
}
key := types.NamespacedName{
Name: "item-changed-secret-type",
Namespace: namespace,
}
toCreate := &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: spec,
}
By("Creating a new OnePasswordItem successfully")
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
By("Reading K8s secret")
secret := &v1.Secret{}
Eventually(func() bool {
err := k8sClient.Get(ctx, key, secret)
return err == nil
}, timeout, interval).Should(BeTrue())
By("Failing to update K8s secret")
Eventually(func() bool {
secret.Type = v1.SecretTypeBasicAuth
err := k8sClient.Update(ctx, secret)
return err == nil
}, timeout, interval).Should(BeFalse())
})
When("OnePasswordItem resource name contains `_`", func() {
It("Should fail creating a OnePasswordItem resource", func() {
ctx := context.Background()
spec := onepasswordv1.OnePasswordItemSpec{
ItemPath: item1.Path,
}
key := types.NamespacedName{
Name: "invalid_name",
Namespace: namespace,
}
toCreate := &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: spec,
}
By("Creating a new OnePasswordItem")
Expect(k8sClient.Create(ctx, toCreate)).To(HaveOccurred())
})
})
When("OnePasswordItem resource name contains capital letters", func() {
It("Should fail creating a OnePasswordItem resource", func() {
ctx := context.Background()
spec := onepasswordv1.OnePasswordItemSpec{
ItemPath: item1.Path,
}
key := types.NamespacedName{
Name: "invalidName",
Namespace: namespace,
}
toCreate := &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: spec,
}
By("Creating a new OnePasswordItem")
Expect(k8sClient.Create(ctx, toCreate)).To(HaveOccurred())
})
})
})
})

View File

@@ -0,0 +1,241 @@
/*
MIT License
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package controller
import (
"context"
"os"
"path/filepath"
"regexp"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/pkg/mocks"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
const (
username = "test-user"
password = "QmHumKc$mUeEem7caHtbaBaJ"
username2 = "test-user2"
password2 = "4zotzqDqXKasLFT2jzTs"
annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
)
// Define utility constants for object names and testing timeouts/durations and intervals.
const (
namespace = "default"
timeout = time.Second * 10
duration = time.Second * 10
interval = time.Millisecond * 250
)
var (
cfg *rest.Config
k8sClient client.Client
testEnv *envtest.Environment
ctx context.Context
cancel context.CancelFunc
onePasswordItemReconciler *OnePasswordItemReconciler
deploymentReconciler *DeploymentReconciler
mockGetItemByIDFunc *mock.Call
item1 = &TestItem{
ItemID: "nwrhuano7bcwddcviubpp4mhfq",
VaultID: "hfnjvi6aymbsnfc2xeeoheizda",
Name: "test-item",
Version: 123,
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizda/items/nwrhuano7bcwddcviubpp4mhfq",
Data: map[string]string{
"username": username,
"password": password,
},
SecretData: map[string][]byte{
"password": []byte(password),
"username": []byte(username),
},
}
item2 = &TestItem{
ItemID: "nwrhuano7bcwddcviubpp4mhf2",
VaultID: "hfnjvi6aymbsnfc2xeeoheizd2",
Name: "test-item2",
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizd2/items/nwrhuano7bcwddcviubpp4mhf2",
Version: 456,
Data: map[string]string{
"username": username2,
"password": password2,
},
SecretData: map[string][]byte{
"password": []byte(password2),
"username": []byte(username2),
},
}
)
type TestItem struct {
ItemID string
VaultID string
Name string
Version int
Path string
Data map[string]string
SecretData map[string][]byte
}
func (ti *TestItem) ToModel() *model.Item {
item := &model.Item{}
item.Version = ti.Version
item.VaultID = ti.VaultID
item.ID = ti.ItemID
item.Fields = []model.ItemField{}
for k, v := range ti.Data {
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
}
return item
}
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
ctx, cancel = context.WithCancel(context.TODO())
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
// Retrieve the first found binary directory to allow running tests from IDEs
if getFirstFoundEnvTestBinaryDir() != "" {
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
}
var err error
// cfg is defined in this file globally.
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
err = onepasswordcomv1.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())
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
Expect(err).ToNot(HaveOccurred())
mockOpClient := &mocks.TestClient{}
mockGetItemByIDFunc = mockOpClient.On("GetItemByID", mock.Anything, mock.Anything)
onePasswordItemReconciler = &OnePasswordItemReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
OpClient: mockOpClient,
}
err = (onePasswordItemReconciler).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())
r, _ := regexp.Compile(annotationRegExpString)
deploymentReconciler = &DeploymentReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
OpClient: mockOpClient,
OpAnnotationRegExp: r,
}
err = (deploymentReconciler).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())
go func() {
defer GinkgoRecover()
err = k8sManager.Start(ctx)
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
}()
})
var _ = AfterSuite(func() {
cancel()
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})
// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
basePath := filepath.Join("..", "..", "bin", "k8s")
entries, err := os.ReadDir(basePath)
if err != nil {
logf.Log.Error(err, "Failed to read directory", "path", basePath)
return ""
}
for _, entry := range entries {
if entry.IsDir() {
return filepath.Join(basePath, entry.Name())
}
}
return ""
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -1,45 +0,0 @@
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// 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
type OnePasswordItemSpec struct {
ItemPath string `json:"itemPath,omitempty"`
}
// OnePasswordItemStatus defines the observed state of OnePasswordItem
type OnePasswordItemStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "operator-sdk generate k8s" 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
// OnePasswordItem is the Schema for the onepassworditems API
// +kubebuilder:subresource:status
// +kubebuilder:resource:path=onepassworditems,scope=Namespaced
type OnePasswordItem struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec OnePasswordItemSpec `json:"spec,omitempty"`
Status OnePasswordItemStatus `json:"status,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// OnePasswordItemList contains a list of OnePasswordItem
type OnePasswordItemList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []OnePasswordItem `json:"items"`
}
func init() {
SchemeBuilder.Register(&OnePasswordItem{}, &OnePasswordItemList{})
}

View File

@@ -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}
)

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -1,206 +0,0 @@
package deployment
import (
"context"
"fmt"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
"github.com/1Password/onepassword-operator/pkg/utils"
"regexp"
"github.com/1Password/connect-sdk-go/connect"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"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_deployment")
var finalizer = "onepassword.com/finalizer.secret"
const annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
func Add(mgr manager.Manager, opConnectClient connect.Client) error {
return add(mgr, newReconciler(mgr, opConnectClient))
}
func newReconciler(mgr manager.Manager, opConnectClient connect.Client) *ReconcileDeployment {
r, _ := regexp.Compile(annotationRegExpString)
return &ReconcileDeployment{
opAnnotationRegExp: r,
kubeClient: mgr.GetClient(),
scheme: mgr.GetScheme(),
opConnectClient: opConnectClient,
}
}
func add(mgr manager.Manager, r reconcile.Reconciler) error {
c, err := controller.New("deployment-controller", mgr, controller.Options{Reconciler: 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
}
var _ reconcile.Reconciler = &ReconcileDeployment{}
type ReconcileDeployment struct {
opAnnotationRegExp *regexp.Regexp
kubeClient client.Client
scheme *runtime.Scheme
opConnectClient connect.Client
}
func (r *ReconcileDeployment) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1.Deployment{}).
Complete(r)
}
func (r *ReconcileDeployment) test() {
return
}
// Reconcile reads that state of the cluster for a Deployment object and makes changes based on the state read
// and what is in the Deployment.Spec
// Note:
// 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.
func (r *ReconcileDeployment) Reconcile(request reconcile.Request) (reconcile.Result, error) {
ctx := context.Background()
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling Deployment")
deployment := &appsv1.Deployment{}
err := r.kubeClient.Get(ctx, request.NamespacedName, deployment)
if err != nil {
if errors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.opAnnotationRegExp)
if !annotationsFound {
reqLogger.Info("No 1Password Annotations found")
return reconcile.Result{}, nil
}
//If the deployment is not being deleted
if deployment.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(deployment.ObjectMeta.Finalizers, finalizer) {
deployment.ObjectMeta.Finalizers = append(deployment.ObjectMeta.Finalizers, finalizer)
if err := r.kubeClient.Update(context.Background(), deployment); err != nil {
return reconcile.Result{}, err
}
}
// Handles creation or updating secrets for deployment if needed
if err := r.HandleApplyingDeployment(deployment.Namespace, annotations, request); err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
// The deployment has been marked for deletion. If the one password
// finalizer is found there are cleanup tasks to perform
if utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
secretName := annotations[op.NameAnnotation]
r.cleanupKubernetesSecretForDeployment(secretName, deployment)
// Remove the finalizer from the deployment so deletion of deployment can be completed
if err := r.removeOnePasswordFinalizerFromDeployment(deployment); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
func (r *ReconcileDeployment) cleanupKubernetesSecretForDeployment(secretName string, deletedDeployment *appsv1.Deployment) error {
kubernetesSecret := &corev1.Secret{}
kubernetesSecret.ObjectMeta.Name = secretName
kubernetesSecret.ObjectMeta.Namespace = deletedDeployment.Namespace
if len(secretName) == 0 {
return nil
}
updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret}
multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment)
if err != nil {
return err
}
// Only delete the associated kubernetes secret if it is not being used by other deployments
if !multipleDeploymentsUsingSecret {
if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil {
if !errors.IsNotFound(err) {
return err
}
}
}
return nil
}
func (r *ReconcileDeployment) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) {
deployments := &appsv1.DeploymentList{}
opts := []client.ListOption{
client.InNamespace(deletedDeployment.Namespace),
}
err := r.kubeClient.List(context.Background(), deployments, opts...)
if err != nil {
log.Error(err, "Failed to list kubernetes deployments")
return false, err
}
for i := 0; i < len(deployments.Items); i++ {
if deployments.Items[i].Name != deletedDeployment.Name {
if op.IsDeploymentUsingSecrets(&deployments.Items[i], updatedSecrets) {
return true, nil
}
}
}
return false, nil
}
func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deployment *appsv1.Deployment) error {
deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer)
return r.kubeClient.Update(context.Background(), deployment)
}
func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotations map[string]string, request reconcile.Request) error {
reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
secretName := annotations[op.NameAnnotation]
secretLabels := map[string]string(nil)
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.")
return nil
}
item, err := op.GetOnePasswordItemByPath(r.opConnectClient, annotations[op.ItemPathAnnotation])
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, annotations)
}

View File

@@ -1,480 +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 Annotations have 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,
},
Labels: map[string]string{},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
Labels: map[string]string(nil),
},
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
}

View File

@@ -1,156 +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"
op "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()
labels := resource.Labels
annotations := resource.Annotations
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)
}
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, annotations)
}

View File

@@ -1,439 +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"
firstHost = "http://localhost:8080"
awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
iceCream = "freezing blue 20%"
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),
op.ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
},
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,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: "456",
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
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,
},
},
{
testName: "Secret from 1Password item with invalid K8s labels",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!my sECReT it3m%",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret-it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Secret from 1Password item with fields and sections that have invalid K8s labels",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!my sECReT it3m%",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret-it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: map[string][]byte{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"ice-cream-type": []byte(iceCream),
},
},
opItem: map[string]string{
userKey: username,
passKey: password,
"first host": firstHost,
"AWS Access Key": awsKey,
"😄 ice-cream type": iceCream,
},
},
{
testName: "Secret from 1Password item with `-`, `_` and `.`",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!.my_sECReT.it3m%-_",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret.it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: map[string][]byte{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"-_ice_cream.type.": []byte(iceCream),
},
},
opItem: map[string]string{
userKey: username,
passKey: password,
"first host": firstHost,
"AWS Access Key": awsKey,
"😄 -_ice_cream.type.": iceCream,
},
},
}
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 = []*onepassword.ItemField{}
for k, v := range testData.opItem {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
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: testData.customResource.ObjectMeta.Name,
Namespace: testData.customResource.ObjectMeta.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
}

View File

@@ -2,18 +2,18 @@ package kubernetessecrets
import (
"context"
"errors"
"fmt"
"reflect"
"regexp"
"strings"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
"github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"reflect"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -23,68 +23,112 @@ import (
const OnepasswordPrefix = "operator.1password.io"
const NameAnnotation = OnepasswordPrefix + "/item-name"
const VersionAnnotation = OnepasswordPrefix + "/item-version"
const restartAnnotation = OnepasswordPrefix + "/last-restarted"
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
var ErrCannotUpdateSecretType = errors.New("cannot change secret type: secret type is immutable")
var log = logf.Log
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretAnnotations map[string]string) error {
func CreateKubernetesSecretFromItem(
ctx context.Context,
kubeClient kubernetesClient.Client,
secretName, namespace string,
item *model.Item,
autoRestart string,
labels map[string]string,
secretType string,
ownerRef *metav1.OwnerReference,
) error {
itemVersion := fmt.Sprint(item.Version)
// If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map
if secretAnnotations == nil {
secretAnnotations = map[string]string{}
secretAnnotations := map[string]string{
VersionAnnotation: itemVersion,
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID),
}
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
return fmt.Errorf("error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false",
RestartDeploymentsAnnotation, secretName,
)
}
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
}
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, *item)
// "Opaque" and "" secret types are treated the same by Kubernetes.
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels,
secretType, *item, ownerRef)
currentSecret := &corev1.Secret{}
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
if err != nil && errors.IsNotFound(err) {
err := kubeClient.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
if err != nil && apierrors.IsNotFound(err) {
log.Info(fmt.Sprintf("Creating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
return kubeClient.Create(context.Background(), secret)
return kubeClient.Create(ctx, secret)
} else if err != nil {
return err
}
if ! reflect.DeepEqual(currentSecret.Annotations, secretAnnotations) || ! reflect.DeepEqual(currentSecret.Labels, labels) {
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
currentSecret.ObjectMeta.Annotations = secretAnnotations
currentSecret.ObjectMeta.Labels = labels
currentSecret.Data = secret.Data
return kubeClient.Update(context.Background(), currentSecret)
// Check if the secret types are being changed on the update.
// Avoid Opaque and "" are treated as different on check.
wantSecretType := secretType
if wantSecretType == "" {
wantSecretType = string(corev1.SecretTypeOpaque)
}
currentSecretType := string(currentSecret.Type)
if currentSecretType == "" {
currentSecretType = string(corev1.SecretTypeOpaque)
}
if currentSecretType != wantSecretType {
return ErrCannotUpdateSecretType
}
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation]))
currentAnnotations := currentSecret.Annotations
currentLabels := currentSecret.Labels
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) {
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
currentSecret.Annotations = secretAnnotations
currentSecret.Labels = labels
currentSecret.Data = secret.Data
if err := kubeClient.Update(ctx, currentSecret); err != nil {
return fmt.Errorf("kubernetes secret update failed: %w", err)
}
return nil
}
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists",
secret.Name, secret.Annotations[VersionAnnotation],
))
return nil
}
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, item onepassword.Item) *corev1.Secret {
func BuildKubernetesSecretFromOnePasswordItem(
name, namespace string,
annotations map[string]string,
labels map[string]string,
secretType string,
item model.Item,
ownerRef *metav1.OwnerReference,
) *corev1.Secret {
var ownerRefs []metav1.OwnerReference
if ownerRef != nil {
ownerRefs = []metav1.OwnerReference{*ownerRef}
}
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: formatSecretName(name),
Namespace: namespace,
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 []model.ItemField, files []model.File) map[string][]byte {
secretData := map[string][]byte{}
for i := 0; i < len(fields); i++ {
if fields[i].Value != "" {
@@ -92,6 +136,23 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byt
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, fmt.Sprintf("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
}

View File

@@ -6,83 +6,137 @@ import (
"strings"
"testing"
"github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
const restartDeploymentAnnotation = "false"
type k8s struct {
clientset kubernetes.Interface
}
const (
restartDeploymentAnnotation = "false"
testNamespace = "test"
testItemUUID = "h46bb3jddvay7nxopfhvlwg35q"
testVaultUUID = "hfnjvi6aymbsnfc2xeeoheizda"
)
func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
ctx := context.Background()
secretName := "test-secret-name"
namespace := "test"
namespace := testNamespace
item := onepassword.Item{}
item := model.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
item.VaultID = testVaultUUID
item.ID = testItemUUID
kubeClient := fake.NewFakeClient()
kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{}
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations)
secretType := ""
err := CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &item, restartDeploymentAnnotation,
secretLabels, secretType, 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)
err = kubeClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
if err != nil {
t.Errorf("Secret was not created: %v", err)
}
compareFields(item.Fields, createdSecret.Data, 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) {
ctx := context.Background()
secretName := "test-secret-name"
namespace := testNamespace
item := model.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.VaultID = testVaultUUID
item.ID = testItemUUID
kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{}
secretType := ""
ownerRef := &metav1.OwnerReference{
Kind: "Deployment",
APIVersion: "apps/v1",
Name: "test-deployment",
UID: types.UID("test-uid"),
}
err := CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &item, restartDeploymentAnnotation,
secretLabels, secretType, ownerRef)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
createdSecret := &corev1.Secret{}
err = kubeClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Check owner references.
gotOwnerRefs := createdSecret.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) {
ctx := context.Background()
secretName := "test-secret-update"
namespace := "test"
namespace := testNamespace
item := onepassword.Item{}
item := model.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
item.VaultID = testVaultUUID
item.ID = testItemUUID
kubeClient := fake.NewFakeClient()
kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{}
secretAnnotations := map[string]string{}
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations)
secretType := ""
err := CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &item, restartDeploymentAnnotation,
secretLabels, secretType, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Updating kubernetes secret with new item
newItem := onepassword.Item{}
newItem := model.Item{}
newItem.Fields = generateFields(6)
newItem.Version = 456
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretAnnotations)
newItem.VaultID = testVaultUUID
newItem.ID = testItemUUID
err = CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation,
secretLabels, secretType, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
updatedSecret := &corev1.Secret{}
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, updatedSecret)
err = kubeClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, updatedSecret)
if err != nil {
t.Errorf("Secret was not found: %v", err)
@@ -93,7 +147,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
func TestBuildKubernetesSecretData(t *testing.T) {
fields := generateFields(5)
secretData := BuildKubernetesSecretData(fields)
secretData := BuildKubernetesSecretData(fields, nil)
if len(secretData) != len(fields) {
t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData))
}
@@ -108,11 +162,12 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
annotations := map[string]string{
annotationKey: annotationValue,
}
item := onepassword.Item{}
item := model.Item{}
item.Fields = generateFields(5)
labels := map[string]string{}
secretType := ""
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item)
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
if kubeSecret.Name != strings.ToLower(name) {
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
}
@@ -133,9 +188,10 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
"annotationKey": "annotationValue",
}
labels := map[string]string{}
item := onepassword.Item{}
item := model.Item{}
secretType := ""
item.Fields = []*onepassword.ItemField{
item.Fields = []model.ItemField{
{
Label: "label w%th invalid ch!rs-",
Value: "value1",
@@ -146,7 +202,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
},
}
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item)
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
// Assert Secret's meta.name was fixed
if kubeSecret.Name != expectedName {
@@ -164,13 +220,45 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
}
}
func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) {
func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
ctx := context.Background()
secretName := "tls-test-secret-name"
namespace := testNamespace
item := model.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.VaultID = testVaultUUID
item.ID = testItemUUID
kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{}
secretType := "kubernetes.io/tls"
err := CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &item, restartDeploymentAnnotation,
secretLabels, secretType, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
createdSecret := &corev1.Secret{}
err = kubeClient.Get(ctx, 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 model.Item, t *testing.T) {
actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
if err != nil {
t.Errorf("Was unable to parse Item Path")
}
if actualVaultId != item.Vault.ID {
t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId)
if actualVaultId != item.VaultID {
t.Errorf("Expected annotation vault id to be %v but was %v", item.VaultID, actualVaultId)
}
if actualItemId != item.ID {
t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId)
@@ -180,11 +268,13 @@ func compareAnnotationsToItem(annotations map[string]string, item onepassword.It
}
if annotations[RestartDeploymentsAnnotation] != "false" {
t.Errorf("Expected restart deployments annotation to be %v but was %v", restartDeploymentAnnotation, RestartDeploymentsAnnotation)
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 []model.ItemField, secretData map[string][]byte, t *testing.T) {
for i := 0; i < len(actualFields); i++ {
value, found := secretData[actualFields[i].Label]
if !found {
@@ -196,14 +286,13 @@ func compareFields(actualFields []*onepassword.ItemField, secretData map[string]
}
}
func generateFields(numToGenerate int) []*onepassword.ItemField {
fields := []*onepassword.ItemField{}
func generateFields(numToGenerate int) []model.ItemField {
fields := []model.ItemField{}
for i := 0; i < numToGenerate; i++ {
field := onepassword.ItemField{
fields = append(fields, model.ItemField{
Label: "key" + fmt.Sprint(i),
Value: "value" + fmt.Sprint(i),
}
fields = append(fields, &field)
})
}
return fields
}
@@ -213,7 +302,10 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
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 validLabel(v string) bool {

11
pkg/logs/log_levels.go Normal file
View File

@@ -0,0 +1,11 @@
package logs
// A Level is a logging priority. Lower levels are more important.
// All levels have been multiplied by -1 to ensure compatibility
// between zapcore and logr
const (
ErrorLevel = -2
WarnLevel = -1
InfoLevel = 0
DebugLevel = 1
)

View File

@@ -1,66 +1,39 @@
package mocks
import (
"github.com/1Password/connect-sdk-go/onepassword"
"context"
"github.com/stretchr/testify/mock"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
type TestClient struct {
GetVaultsFunc func() ([]onepassword.Vault, error)
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, 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
mock.Mock
}
var (
GetGetVaultsFunc func() ([]onepassword.Vault, error)
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
DoGetItemByTitleFunc func(title string, 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)
)
// Do is the mock client's `Do` func
func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
return GetGetVaultsFunc()
func (tc *TestClient) GetItemByID(ctx context.Context, vaultID, itemID string) (*model.Item, error) {
args := tc.Called(vaultID, itemID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.Item), args.Error(1)
}
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
return DoGetVaultsByTitleFunc(title)
func (tc *TestClient) GetItemsByTitle(ctx context.Context, vaultID, itemTitle string) ([]model.Item, error) {
args := tc.Called(vaultID, itemTitle)
return args.Get(0).([]model.Item), args.Error(1)
}
func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
return GetGetItemFunc(uuid, vaultUUID)
func (tc *TestClient) GetFileContent(ctx context.Context, vaultID, itemID, fileID string) ([]byte, error) {
args := tc.Called(vaultID, itemID, fileID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}
func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
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) {
return DoGetItemByTitleFunc(title, vaultUUID)
}
func (m *TestClient) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
return DoCreateItemFunc(item, vaultUUID)
}
func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error {
return DoDeleteItemFunc(item, vaultUUID)
}
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
return DoUpdateItemFunc(item, vaultUUID)
func (tc *TestClient) GetVaultsByTitle(ctx context.Context, title string) ([]model.Vault, error) {
args := tc.Called(title)
return args.Get(0).([]model.Vault), args.Error(1)
}

View File

@@ -45,13 +45,14 @@ func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[
func AreAnnotationsUsingSecrets(annotations map[string]string, secrets map[string]*corev1.Secret) bool {
_, ok := secrets[annotations[NameAnnotation]]
if ok {
return true
}
return false
return ok
}
func AppendAnnotationUpdatedSecret(annotations map[string]string, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret {
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

View File

@@ -80,7 +80,7 @@ func TestGetNoAnnotationsForDeployment(t *testing.T) {
}
numAnnotations := len(filteredAnnotations)
if 0 != numAnnotations {
if numAnnotations != 0 {
t.Errorf("Expected %v annotations got %v", 0, numAnnotations)
}
}

View File

@@ -0,0 +1,56 @@
package client
import (
"context"
"errors"
"os"
"github.com/go-logr/logr"
"github.com/1Password/onepassword-operator/pkg/onepassword/client/connect"
"github.com/1Password/onepassword-operator/pkg/onepassword/client/sdk"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
// Client is an interface for interacting with 1Password items and vaults.
type Client interface {
GetItemByID(ctx context.Context, vaultID, itemID string) (*model.Item, error)
GetItemsByTitle(ctx context.Context, vaultID, itemTitle string) ([]model.Item, error)
GetFileContent(ctx context.Context, vaultID, itemID, fileID string) ([]byte, error)
GetVaultsByTitle(ctx context.Context, title string) ([]model.Vault, error)
}
type Config struct {
Logger logr.Logger
Version string
}
// NewFromEnvironment creates a new 1Password client based on the provided configuration.
func NewFromEnvironment(ctx context.Context, cfg Config) (Client, error) {
connectHost, _ := os.LookupEnv("OP_CONNECT_HOST")
connectToken, _ := os.LookupEnv("OP_CONNECT_TOKEN")
serviceAccountToken, _ := os.LookupEnv("OP_SERVICE_ACCOUNT_TOKEN")
if connectHost != "" && connectToken != "" && serviceAccountToken != "" {
return nil, errors.New("invalid configuration. Either Connect or Service Account credentials should be set, not both")
}
if serviceAccountToken != "" {
cfg.Logger.Info("Using Service Account Token")
return sdk.NewClient(ctx, sdk.Config{
ServiceAccountToken: serviceAccountToken,
IntegrationName: "1password-operator",
IntegrationVersion: cfg.Version,
})
}
if connectHost != "" && connectToken != "" {
cfg.Logger.Info("Using 1Password Connect")
return connect.NewClient(connect.Config{
ConnectHost: connectHost,
ConnectToken: connectToken,
}), nil
}
return nil, errors.New("invalid configuration. Connect or Service Account credentials should be set")
}

View File

@@ -0,0 +1,104 @@
package connect
import (
"context"
"errors"
"fmt"
"time"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
// Config holds the configuration for the Connect client.
type Config struct {
ConnectHost string
ConnectToken string
}
// Connect is a client for interacting with 1Password using the Connect API.
type Connect struct {
client connect.Client
}
// NewClient creates a new Connect client using provided configuration.
func NewClient(config Config) *Connect {
return &Connect{
client: connect.NewClient(config.ConnectHost, config.ConnectToken),
}
}
func (c *Connect) GetItemByID(ctx context.Context, vaultID, itemID string) (*model.Item, error) {
connectItem, err := c.client.GetItemByUUID(itemID, vaultID)
if err != nil {
return nil, fmt.Errorf("failed to GetItemByID using 1Password Connect: %w", err)
}
var item model.Item
item.FromConnectItem(connectItem)
return &item, nil
}
func (c *Connect) GetItemsByTitle(ctx context.Context, vaultID, itemTitle string) ([]model.Item, error) {
// Get all items in the vault with the specified title
connectItems, err := c.client.GetItemsByTitle(itemTitle, vaultID)
if err != nil {
return nil, fmt.Errorf("failed to GetItemsByTitle using 1Password Connect: %w", err)
}
items := make([]model.Item, len(connectItems))
for i, connectItem := range connectItems {
var item model.Item
item.FromConnectItem(&connectItem)
items[i] = item
}
return items, nil
}
// GetFileContent retrieves the content of a file from a 1Password item.
// As the Connect has a delay when synchronizing files and returns a 500 error in this case,
// this function implements a retry mechanism.
func (c *Connect) GetFileContent(ctx context.Context, vaultID, itemID, fileID string) ([]byte, error) {
const maxRetries = 5
const delay = 1 * time.Second
var lastErr error
for i := 0; i < maxRetries; i++ {
bytes, err := c.client.GetFileContent(&onepassword.File{
ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", vaultID, itemID, fileID),
})
if err == nil {
return bytes, nil
}
var connectErr *onepassword.Error
if errors.As(err, &connectErr) && connectErr.StatusCode == 500 {
lastErr = err
time.Sleep(delay)
continue
}
return nil, fmt.Errorf("failed to GetFileContent using 1Password Connect: %w", err)
}
return nil, fmt.Errorf("failed to GetFileContent using 1Password Connect after %d retries: %w", maxRetries, lastErr)
}
func (c *Connect) GetVaultsByTitle(ctx context.Context, vaultQuery string) ([]model.Vault, error) {
connectVaults, err := c.client.GetVaultsByTitle(vaultQuery)
if err != nil {
return nil, fmt.Errorf("failed to GetVaultsByTitle using 1Password Connect: %w", err)
}
var vaults []model.Vault
for _, connectVault := range connectVaults {
if vaultQuery == connectVault.Name {
var vault model.Vault
vault.FromConnectVault(&connectVault)
vaults = append(vaults, vault)
}
}
return vaults, nil
}

View File

@@ -0,0 +1,241 @@
package connect
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/1Password/connect-sdk-go/onepassword"
clienttesting "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing"
"github.com/1Password/onepassword-operator/pkg/onepassword/client/testing/mock"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
const VaultTitleEmployee = "Employee"
func TestConnect_GetItemByID(t *testing.T) {
connectItem := clienttesting.CreateConnectItem()
testCases := map[string]struct {
mockClient func() *mock.ConnectClientMock
check func(t *testing.T, item *model.Item, err error)
}{
"should return an item": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemByUUID", "item-id", "vault-id").Return(connectItem, nil)
return mockConnectClient
},
check: func(t *testing.T, item *model.Item, err error) {
require.NoError(t, err)
clienttesting.CheckConnectItemMapping(t, connectItem, item)
},
},
"should return an error": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemByUUID", "item-id", "vault-id").Return((*onepassword.Item)(nil), errors.New("error"))
return mockConnectClient
},
check: func(t *testing.T, item *model.Item, err error) {
require.Error(t, err)
require.Nil(t, item)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &Connect{client: tc.mockClient()}
item, err := client.GetItemByID(context.Background(), "vault-id", "item-id")
tc.check(t, item, err)
})
}
}
func TestConnect_GetItemsByTitle(t *testing.T) {
connectItem1 := clienttesting.CreateConnectItem()
connectItem2 := clienttesting.CreateConnectItem()
testCases := map[string]struct {
mockClient func() *mock.ConnectClientMock
check func(t *testing.T, items []model.Item, err error)
}{
"should return a single item": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return(
[]onepassword.Item{
*connectItem1,
}, nil)
return mockConnectClient
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 1)
require.Equal(t, connectItem1.ID, items[0].ID)
},
},
"should return two items": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return(
[]onepassword.Item{
*connectItem1,
*connectItem2,
}, nil)
return mockConnectClient
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 2)
clienttesting.CheckConnectItemMapping(t, connectItem1, &items[0])
clienttesting.CheckConnectItemMapping(t, connectItem2, &items[1])
},
},
"should return an error": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return([]onepassword.Item{}, errors.New("error"))
return mockConnectClient
},
check: func(t *testing.T, items []model.Item, err error) {
require.Error(t, err)
require.Nil(t, items)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &Connect{client: tc.mockClient()}
items, err := client.GetItemsByTitle(context.Background(), "vault-id", "item-title")
tc.check(t, items, err)
})
}
}
func TestConnect_GetFileContent(t *testing.T) {
testCases := map[string]struct {
mockClient func() *mock.ConnectClientMock
check func(t *testing.T, content []byte, err error)
}{
"should return file content": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetFileContent", &onepassword.File{
ContentPath: "/v1/vaults/vault-id/items/item-id/files/file-id/content",
}).Return([]byte("file content"), nil)
return mockConnectClient
},
check: func(t *testing.T, content []byte, err error) {
require.NoError(t, err)
require.Equal(t, []byte("file content"), content)
},
},
"should return an error": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetFileContent", &onepassword.File{
ContentPath: "/v1/vaults/vault-id/items/item-id/files/file-id/content",
}).Return(nil, errors.New("error"))
return mockConnectClient
},
check: func(t *testing.T, content []byte, err error) {
require.Error(t, err)
require.Nil(t, content)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &Connect{client: tc.mockClient()}
content, err := client.GetFileContent(context.Background(), "vault-id", "item-id", "file-id")
tc.check(t, content, err)
})
}
}
func TestConnect_GetVaultsByTitle(t *testing.T) {
now := time.Now()
testCases := map[string]struct {
mockClient func() *mock.ConnectClientMock
check func(t *testing.T, vaults []model.Vault, err error)
}{
"should return a single vault": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{
{
ID: "test-id",
Name: VaultTitleEmployee,
CreatedAt: now,
},
{
ID: "test-id-2",
Name: "Some other vault",
CreatedAt: now,
},
}, nil)
return mockConnectClient
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.NoError(t, err)
require.Len(t, vaults, 1)
require.Equal(t, "test-id", vaults[0].ID)
require.Equal(t, now, vaults[0].CreatedAt)
},
},
"should return a two vaults": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{
{
ID: "test-id",
Name: VaultTitleEmployee,
CreatedAt: now,
},
{
ID: "test-id-2",
Name: VaultTitleEmployee,
CreatedAt: now,
},
}, nil)
return mockConnectClient
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.NoError(t, err)
require.Len(t, vaults, 2)
// Check the first vault
require.Equal(t, "test-id", vaults[0].ID)
require.Equal(t, now, vaults[0].CreatedAt)
// Check the second vault
require.Equal(t, "test-id-2", vaults[1].ID)
require.Equal(t, now, vaults[1].CreatedAt)
},
},
"should return an error": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{}, errors.New("error"))
return mockConnectClient
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.Error(t, err)
require.Empty(t, vaults)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &Connect{client: tc.mockClient()}
vault, err := client.GetVaultsByTitle(context.Background(), VaultTitleEmployee)
tc.check(t, vault, err)
})
}
}

View File

@@ -0,0 +1,97 @@
package sdk
import (
"context"
"fmt"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
sdk "github.com/1password/onepassword-sdk-go"
)
// Config holds the configuration for the 1Password SDK client.
type Config struct {
ServiceAccountToken string
IntegrationName string
IntegrationVersion string
}
// SDK is a client for interacting with 1Password using the SDK.
type SDK struct {
client *sdk.Client
}
func NewClient(ctx context.Context, config Config) (*SDK, error) {
client, err := sdk.NewClient(ctx,
sdk.WithServiceAccountToken(config.ServiceAccountToken),
sdk.WithIntegrationInfo(config.IntegrationName, config.IntegrationVersion),
)
if err != nil {
return nil, fmt.Errorf("1Password sdk error: %w", err)
}
return &SDK{
client: client,
}, nil
}
func (s *SDK) GetItemByID(ctx context.Context, vaultID, itemID string) (*model.Item, error) {
sdkItem, err := s.client.Items().Get(ctx, vaultID, itemID)
if err != nil {
return nil, fmt.Errorf("failed to GetItemsByTitle using 1Password SDK: %w", err)
}
var item model.Item
item.FromSDKItem(&sdkItem)
return &item, nil
}
func (s *SDK) GetItemsByTitle(ctx context.Context, vaultID, itemTitle string) ([]model.Item, error) {
// Get all items in the vault
sdkItems, err := s.client.Items().List(ctx, vaultID)
if err != nil {
return nil, fmt.Errorf("failed to GetItemsByTitle using 1Password SDK: %w", err)
}
// Filter items by title
var items []model.Item
for _, sdkItem := range sdkItems {
if sdkItem.Title == itemTitle {
var item model.Item
item.FromSDKItemOverview(&sdkItem)
items = append(items, item)
}
}
return items, nil
}
func (s *SDK) GetFileContent(ctx context.Context, vaultID, itemID, fileID string) ([]byte, error) {
bytes, err := s.client.Items().Files().Read(ctx, vaultID, itemID, sdk.FileAttributes{
ID: fileID,
})
if err != nil {
return nil, fmt.Errorf("failed to GetFileContent using 1Password SDK: %w", err)
}
return bytes, nil
}
func (s *SDK) GetVaultsByTitle(ctx context.Context, title string) ([]model.Vault, error) {
// List all vaults
sdkVaults, err := s.client.Vaults().List(ctx)
if err != nil {
return nil, fmt.Errorf("failed to GetVaultsByTitle using 1Password SDK: %w", err)
}
// Filter vaults by title
var vaults []model.Vault
for _, sdkVault := range sdkVaults {
if sdkVault.Title == title {
var vault model.Vault
vault.FromSDKVault(&sdkVault)
vaults = append(vaults, vault)
}
}
return vaults, nil
}

View File

@@ -0,0 +1,288 @@
package sdk
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
clienttesting "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing"
clientmock "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing/mock"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
sdk "github.com/1password/onepassword-sdk-go"
)
const VaultTitleEmployee = "Employee"
func TestSDK_GetItemByID(t *testing.T) {
sdkItem := clienttesting.CreateSDKItem()
testCases := map[string]struct {
mockItemAPI func() *clientmock.ItemAPIMock
check func(t *testing.T, item *model.Item, err error)
}{
"should return a single item": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("Get", context.Background(), "vault-id", "item-id").Return(*sdkItem, nil)
return m
},
check: func(t *testing.T, item *model.Item, err error) {
require.NoError(t, err)
clienttesting.CheckSDKItemMapping(t, sdkItem, item)
},
},
"should return an error": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("Get", context.Background(), "vault-id", "item-id").Return(sdk.Item{}, errors.New("error"))
return m
},
check: func(t *testing.T, item *model.Item, err error) {
require.Error(t, err)
require.Empty(t, item)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &SDK{
client: &sdk.Client{
ItemsAPI: tc.mockItemAPI(),
},
}
item, err := client.GetItemByID(context.Background(), "vault-id", "item-id")
tc.check(t, item, err)
})
}
}
func TestSDK_GetItemsByTitle(t *testing.T) {
sdkItem1 := clienttesting.CreateSDKItemOverview()
sdkItem2 := clienttesting.CreateSDKItemOverview()
testCases := map[string]struct {
mockItemAPI func() *clientmock.ItemAPIMock
check func(t *testing.T, items []model.Item, err error)
}{
"should return a single item": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
copySDKItem2 := *sdkItem2
copySDKItem2.Title = "Some other item"
m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{
*sdkItem1,
copySDKItem2,
}, nil)
return m
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 1)
clienttesting.CheckSDKItemOverviewMapping(t, sdkItem1, &items[0])
},
},
"should return a two items": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{
*sdkItem1,
*sdkItem2,
}, nil)
return m
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 2)
clienttesting.CheckSDKItemOverviewMapping(t, sdkItem1, &items[0])
clienttesting.CheckSDKItemOverviewMapping(t, sdkItem2, &items[1])
},
},
"should return empty list": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{}, nil)
return m
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 0)
},
},
"should return an error": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{}, errors.New("error"))
return m
},
check: func(t *testing.T, items []model.Item, err error) {
require.Error(t, err)
require.Empty(t, items)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &SDK{
client: &sdk.Client{
ItemsAPI: tc.mockItemAPI(),
},
}
items, err := client.GetItemsByTitle(context.Background(), "vault-id", "item-title")
tc.check(t, items, err)
})
}
}
func TestSDK_GetFileContent(t *testing.T) {
testCases := map[string]struct {
mockItemAPI func() *clientmock.ItemAPIMock
check func(t *testing.T, content []byte, err error)
}{
"should return file content": {
mockItemAPI: func() *clientmock.ItemAPIMock {
fileMock := &clientmock.FileAPIMock{}
fileMock.On("Read", mock.Anything, "vault-id", "item-id",
mock.MatchedBy(func(attr sdk.FileAttributes) bool {
return attr.ID == "file-id"
}),
).Return([]byte("file content"), nil)
itemMock := &clientmock.ItemAPIMock{
FilesAPI: fileMock,
}
itemMock.On("Files").Return(fileMock)
return itemMock
},
check: func(t *testing.T, content []byte, err error) {
require.NoError(t, err)
require.Equal(t, []byte("file content"), content)
},
},
"should return an error": {
mockItemAPI: func() *clientmock.ItemAPIMock {
fileMock := &clientmock.FileAPIMock{}
fileMock.On("Read", mock.Anything, "vault-id", "item-id",
mock.MatchedBy(func(attr sdk.FileAttributes) bool {
return attr.ID == "file-id"
}),
).Return(nil, errors.New("error"))
itemMock := &clientmock.ItemAPIMock{
FilesAPI: fileMock,
}
itemMock.On("Files").Return(fileMock)
return itemMock
},
check: func(t *testing.T, content []byte, err error) {
require.Error(t, err)
require.Nil(t, content)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &SDK{
client: &sdk.Client{
ItemsAPI: tc.mockItemAPI(),
},
}
content, err := client.GetFileContent(context.Background(), "vault-id", "item-id", "file-id")
tc.check(t, content, err)
})
}
}
func TestSDK_GetVaultsByTitle(t *testing.T) {
now := time.Now()
testCases := map[string]struct {
mockVaultAPI func() *clientmock.VaultAPIMock
check func(t *testing.T, vaults []model.Vault, err error)
}{
"should return a single vault": {
mockVaultAPI: func() *clientmock.VaultAPIMock {
m := &clientmock.VaultAPIMock{}
m.On("List", context.Background()).Return([]sdk.VaultOverview{
{
ID: "test-id",
Title: VaultTitleEmployee,
CreatedAt: now,
},
{
ID: "test-id-2",
Title: "Some other vault",
CreatedAt: now,
},
}, nil)
return m
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.NoError(t, err)
require.Len(t, vaults, 1)
require.Equal(t, "test-id", vaults[0].ID)
require.Equal(t, now, vaults[0].CreatedAt)
},
},
"should return a two vaults": {
mockVaultAPI: func() *clientmock.VaultAPIMock {
m := &clientmock.VaultAPIMock{}
m.On("List", context.Background()).Return([]sdk.VaultOverview{
{
ID: "test-id",
Title: VaultTitleEmployee,
CreatedAt: now,
},
{
ID: "test-id-2",
Title: VaultTitleEmployee,
CreatedAt: now,
},
}, nil)
return m
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.NoError(t, err)
require.Len(t, vaults, 2)
// Check the first vault
require.Equal(t, "test-id", vaults[0].ID)
require.Equal(t, now, vaults[0].CreatedAt)
// Check the second vault
require.Equal(t, "test-id-2", vaults[1].ID)
require.Equal(t, now, vaults[1].CreatedAt)
},
},
"should return an error": {
mockVaultAPI: func() *clientmock.VaultAPIMock {
m := &clientmock.VaultAPIMock{}
m.On("List", context.Background()).Return([]sdk.VaultOverview{}, errors.New("error"))
return m
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.Error(t, err)
require.Empty(t, vaults)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &SDK{
client: &sdk.Client{
VaultsAPI: tc.mockVaultAPI(),
},
}
vault, err := client.GetVaultsByTitle(context.Background(), VaultTitleEmployee)
tc.check(t, vault, err)
})
}
}

Some files were not shown because too many files have changed in this diff Show More