Compare commits

...

175 Commits

Author SHA1 Message Date
Volodymyr Zotov
316f605680 Update docs 2025-10-02 13:18:05 -05:00
Volodymyr Zotov
7135f439a5 Update check-external-pr check when e2e test success 2025-10-02 13:14:04 -05:00
Volodymyr Zotov
625297c1fb Add doc explaining how to test forked PR 2025-10-02 12:41:01 -05:00
Volodymyr Zotov
e967f7ce1e Revert job name 2025-10-02 12:40:35 -05:00
Volodymyr Zotov
5199e9f350 Revert "Update status check to 'success' only if e2e tests are passed"
This reverts commit 878b38deac.
2025-10-02 12:37:04 -05:00
Volodymyr Zotov
878b38deac Update status check to 'success' only if e2e tests are passed 2025-10-02 12:36:27 -05:00
Volodymyr Zotov
1a40b4fb95 Pass jobm name as env 2025-10-02 12:35:06 -05:00
Volodymyr Zotov
4b5d3c5f1a Use propper job name e2e-tests 2025-10-02 12:34:10 -05:00
Volodymyr Zotov
8d4908fb72 Updated comment to mention sha= portion to dispatch the event 2025-10-02 11:50:15 -05:00
Volodymyr Zotov
6cf52a7bfc Use process.env.job instead of hardcoding job name 2025-10-02 11:29:31 -05:00
Volodymyr Zotov
7ef9ec6de7 Update e2e job names to avoid confustion and fix updating check status 2025-10-02 08:35:37 -05:00
Volodymyr Zotov
3f52bb2840 Add new line at the end of the file 2025-10-02 08:09:04 -05:00
Volodymyr Zotov
63e3f29be9 Refactor e2e test workflows 2025-09-30 21:44:16 -05:00
Volodymyr Zotov
49bc9cb329 Use workflow anchors 2025-09-30 21:37:11 -05:00
Volodymyr Zotov
de62e07bcf Bump action versions in other workflows 2025-09-11 12:20:40 -05:00
Volodymyr Zotov
3ebc536dd7 Add empty line at the end of the file 2025-09-11 11:50:02 -05:00
Volodymyr Zotov
6769e25a98 Do not run e2e tests when making a change on documentation or not realted to the operator files 2025-09-11 11:49:09 -05:00
Volodymyr Zotov
d8734c9ae3 Run e2e tests when pusing to main and bump actions to the latest 2025-09-11 11:16:47 -05:00
Volodymyr Zotov
460742869b Add workflow to run e2e tests on contributor's branch 2025-09-11 11:14:10 -05:00
Volodymyr Zotov
35e476230c Add ok-to-test workflow 2025-09-11 10:52:53 -05:00
Volodymyr Zotov
3a9691576a Add ok to test workflow 2025-09-05 13:45:29 -05:00
Volodymyr Zotov
94602ddd72 Fix lint errors 2025-09-05 11:34:18 -05:00
Volodymyr Zotov
292c6f0e93 Update testing doc to mention integration and unit tests under single command 2025-09-05 11:24:02 -05:00
Volodymyr Zotov
0f1293ca95 Update testing doc to merge integration and unit tests under single command 2025-09-05 11:20:08 -05:00
Volodymyr Zotov
706ebdd8b8 Copy manager.yaml from test/e2e when starting e2e tests 2025-09-05 10:40:46 -05:00
Volodymyr Zotov
bd963bcd1d Revert config/manager.yaml 2025-09-05 10:40:45 -05:00
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
4209 changed files with 4787 additions and 1474596 deletions

View File

@@ -1 +1 @@
1.8.1 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

View File

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

52
.github/workflows/e2e-tests.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: E2E Tests [reusable]
on:
workflow_call:
secrets:
OP_CONNECT_CREDENTIALS:
description: '1Password Connect credentials'
required: true
OP_CONNECT_TOKEN:
description: '1Password Connect token'
required: true
OP_SERVICE_ACCOUNT_TOKEN:
description: '1Password service account token'
required: true
jobs:
e2e-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v6
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/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@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run linter
uses: golangci/golangci-lint-action@v8
with:
version: v2.2

25
.github/workflows/ok-to-test.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Write comments "/ok-to-test sha=<hash>" on a pull request. This will emit a repository_dispatch event.
name: Ok To Test
on:
issue_comment:
types: [created]
jobs:
ok-to-test:
runs-on: ubuntu-latest
permissions:
pull-requests: write # For adding reactions to the pull request comments
contents: write # For executing the repository_dispatch event
# Only run for PRs, not issue comments
if: ${{ github.event.issue.pull_request }}
steps:
- name: Slash Command Dispatch
uses: volodymyrZotov/slash-command-dispatch@7c1b623a2b0eba93f684c34f689a441f0be84cf1 # TODO: use peter-evans/slash-command-dispatch when fix for team permissions is released https://github.com/peter-evans/slash-command-dispatch/pull/424
with:
token: ${{ secrets.GITHUB_TOKEN }}
reaction-token: ${{ secrets.GITHUB_TOKEN }}
issue-type: pull-request
commands: ok-to-test
# The repository permission level required by the user to dispatch commands. Only allows 1Password collaborators to run this.
permission: write

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

@@ -17,7 +17,7 @@ jobs:
- -
id: is_release_branch_without_pr id: is_release_branch_without_pr
name: Find matching PR name: Find matching PR
uses: actions/github-script@v6 uses: actions/github-script@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
@@ -43,7 +43,7 @@ jobs:
name: Create Release Pull Request name: Create Release Pull Request
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v5
- name: Parse release version - name: Parse release version
id: get_version id: get_version

View File

@@ -12,13 +12,13 @@ jobs:
DOCKER_CLI_EXPERIMENTAL: "enabled" DOCKER_CLI_EXPERIMENTAL: "enabled"
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: | images: |
1password/onepassword-operator 1password/onepassword-operator
@@ -33,19 +33,19 @@ jobs:
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Docker Login - name: Docker Login
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: Dockerfile file: Dockerfile

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

@@ -0,0 +1,57 @@
name: E2E tests [fork]
on:
repository_dispatch:
types: [ ok-to-test-command ]
permissions:
contents: read
checks: write
concurrency:
group: e2e-fork-${{ github.event.client_payload.pull_request.number || github.run_id }}
cancel-in-progress: true # cancel previous job runs for the same branch
jobs:
e2e-tests:
uses: ./.github/workflows/e2e-tests.yml
if: |
github.event_name == 'repository_dispatch' &&
github.event.client_payload.slash_command.args.named.sha != '' &&
contains(
github.event.client_payload.pull_request.head.sha,
github.event.client_payload.slash_command.args.named.sha
)
secrets:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
update-check-status:
needs: e2e-tests
runs-on: ubuntu-latest
if: always() && github.event_name == 'repository_dispatch'
steps:
- uses: actions/github-script@v6
env:
ref: ${{ github.event.client_payload.pull_request.head.sha }}
conclusion: ${{ needs.e2e-tests.result }}
job: check-external-pr
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: checks } = await github.rest.checks.listForRef({
...context.repo,
ref: process.env.ref
});
const check = checks.check_runs.filter(c => c.name === process.env.job);
const { data: result } = await github.rest.checks.update({
...context.repo,
check_run_id: check[0].id,
status: 'completed',
conclusion: process.env.conclusion
});
return result;

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

@@ -0,0 +1,43 @@
name: E2E Tests
on:
pull_request:
types: [opened, synchronize, reopened]
branches: ['**'] # run for PRs targeting any branch (main and others)
paths-ignore: &ignore_paths
- 'docs/**'
- '*.md'
- '.golangci.yml'
- '.gitignore'
- '.dockerignore'
- 'LICENSE'
push:
branches: [main]
paths-ignore: *ignore_paths
concurrency:
group: e2e-${{ github.event.pull_request.head.ref }}
cancel-in-progress: true # cancel previous job runs for the same branch
jobs:
check-external-pr:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Check if PR is from external contributor
run: |
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "❌ External PR detected. This workflow requires approval from a maintainer."
echo "Please ask a maintainer to run '/ok-to-test' command to trigger the fork workflow."
exit 1
fi
echo "✅ Internal PR detected. Proceeding with tests."
e2e-test:
needs: check-external-pr
if: always() && (needs.check-external-pr.result == 'success' || github.event_name != 'pull_request')
uses: ./.github/workflows/e2e-tests.yml
secrets:
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
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@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Running Tests
run: |
go mod tidy
make test

9
.gitignore vendored
View File

@@ -8,14 +8,16 @@
bin bin
testbin/* testbin/*
# Test binary, build with `go test -c` # Test binary, built with `go test -c`
*.test *.test
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Kubernetes Generated files - skip generated files, except for vendored files # Go workspace file
go.work
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.* !vendor/**/zz_generated.*
# editor and IDE paraphernalia # editor and IDE paraphernalia
@@ -23,3 +25,6 @@ testbin/*
*.swp *.swp
*.swo *.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,6 +12,30 @@
--- ---
[//]: # (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) [//]: # (START/v1.8.1)
# v1.8.1 # v1.8.1

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,5 +1,5 @@
# Build the manager binary # Build the manager binary
FROM golang:1.21 as builder FROM golang:1.24 AS builder
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@@ -8,25 +8,28 @@ WORKDIR /workspace
COPY go.mod go.mod COPY go.mod go.mod
COPY go.sum go.sum COPY go.sum go.sum
# Download dependencies
RUN go mod download
# Copy the go source # Copy the go source
COPY cmd/main.go cmd/main.go COPY cmd/main.go cmd/main.go
COPY api/ api/ COPY api/ api/
COPY internal/controller/ internal/controller/ COPY internal/controller/ internal/controller/
COPY pkg/ pkg/ COPY pkg/ pkg/
COPY version/ version/ COPY version/ version/
COPY vendor/ vendor/
# Build # Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command # 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 # 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, # 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. # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 \ RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 \
GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build \ go build \
-ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \ -ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \
-mod vendor \ -o manager cmd/main.go
-a -o manager cmd/main.go
# Use distroless as minimal base image to package the manager binary # Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details # Refer to https://github.com/GoogleContainerTools/distroless for more details

160
Makefile
View File

@@ -5,7 +5,11 @@ export MAIN_BRANCH ?= main
# To re-generate a bundle for another specific version without changing the standard setup, you can: # 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 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) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
VERSION ?= 1.8.1 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. # 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") # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
@@ -50,12 +54,10 @@ endif
# Set the Operator SDK version to use. By default, what is installed on the system is used. # 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. # This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
OPERATOR_SDK_VERSION ?= v1.33.0 OPERATOR_SDK_VERSION ?= v1.41.1
# Image URL to use all building/pushing image targets # Image URL to use all building/pushing image targets
IMG ?= 1password/onepassword-operator:latest IMG ?= 1password/onepassword-operator:latest
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.28.3
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN)) ifeq (,$(shell go env GOBIN))
@@ -82,7 +84,7 @@ all: build
# The help target prints out all targets with their descriptions organized # The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the # beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading 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 # 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, # 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. # if there's a line with ##@ something, that gets pretty-printed as a category.
@@ -114,8 +116,49 @@ vet: ## Run go vet against code.
go vet ./... go vet ./...
.PHONY: test .PHONY: test
test: manifests generate fmt vet envtest ## Run tests. test: manifests generate fmt vet setup-envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out 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 ##@ Build
@@ -127,34 +170,40 @@ build: manifests generate fmt vet ## Build manager binary.
run: manifests generate fmt vet ## Run a controller from your host. run: manifests generate fmt vet ## Run a controller from your host.
go run ./cmd/main.go go run ./cmd/main.go
# If you wish built the manager image targeting other platforms you can use the --platform flag. # 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. # (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/ # More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build .PHONY: docker-build
docker-build: test ## Build docker image with the manager. docker-build: ## Build docker image with the manager.
$(CONTAINER_TOOL) build -t ${IMG} . DOCKER_BUILDKIT=1 $(CONTAINER_TOOL) build -t ${IMG} .
.PHONY: docker-push .PHONY: docker-push
docker-push: ## Push docker image with the manager. docker-push: ## Push docker image with the manager.
$(CONTAINER_TOOL) push ${IMG} $(CONTAINER_TOOL) push ${IMG}
# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple # 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: # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=<myregistry/image:<tag>> then the export will fail) # - 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 properly provided solutions that supports more than one platform you should use this option. # 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 PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
.PHONY: docker-buildx .PHONY: docker-buildx
docker-buildx: test ## Build and push docker image for the manager for cross-platform support 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 # 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 sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
- $(CONTAINER_TOOL) buildx create --name project-v3-builder - $(CONTAINER_TOOL) buildx create --name onepassword-operator-builder
$(CONTAINER_TOOL) buildx use project-v3-builder $(CONTAINER_TOOL) buildx use onepassword-operator-builder
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
- $(CONTAINER_TOOL) buildx rm project-v3-builder - $(CONTAINER_TOOL) buildx rm onepassword-operator-builder
rm Dockerfile.cross 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 ##@ Deployment
ifndef ignore-not-found ifndef ignore-not-found
@@ -179,10 +228,14 @@ deploy: manifests kustomize set-namespace ## Deploy controller to the K8s cluste
$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
.PHONY: undeploy .PHONY: undeploy
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 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 - $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
##@ Build Dependencies .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 ## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin LOCALBIN ?= $(shell pwd)/bin
@@ -191,33 +244,64 @@ $(LOCALBIN):
## Tool Binaries ## Tool Binaries
KUBECTL ?= kubectl KUBECTL ?= kubectl
KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
## Tool Versions ## Tool Versions
KUSTOMIZE_VERSION ?= v5.3.0 KUSTOMIZE_VERSION ?= v5.6.0
CONTROLLER_TOOLS_VERSION ?= v0.13.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 .PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN) $(KUSTOMIZE): $(LOCALBIN)
@if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \
rm -rf $(LOCALBIN)/kustomize; \
fi
test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION)
.PHONY: controller-gen .PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN) $(CONTROLLER_GEN): $(LOCALBIN)
test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
GOBIN=$(LOCALBIN) go install 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 .PHONY: envtest
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN) $(ENVTEST): $(LOCALBIN)
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest $(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 .PHONY: operator-sdk
OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk
@@ -245,14 +329,14 @@ bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metada
.PHONY: bundle-build .PHONY: bundle-build
bundle-build: ## Build the bundle image. bundle-build: ## Build the bundle image.
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . $(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
.PHONY: bundle-push .PHONY: bundle-push
bundle-push: ## Push the bundle image. bundle-push: ## Push the bundle image.
$(MAKE) docker-push IMG=$(BUNDLE_IMG) $(MAKE) docker-push IMG=$(BUNDLE_IMG)
.PHONY: opm .PHONY: opm
OPM = ./bin/opm OPM = $(LOCALBIN)/opm
opm: ## Download opm locally if necessary. opm: ## Download opm locally if necessary.
ifeq (,$(wildcard $(OPM))) ifeq (,$(wildcard $(OPM)))
ifeq (,$(shell which opm 2>/dev/null)) ifeq (,$(shell which opm 2>/dev/null))
@@ -260,7 +344,7 @@ ifeq (,$(shell which opm 2>/dev/null))
set -e ;\ set -e ;\
mkdir -p $(dir $(OPM)) ;\ mkdir -p $(dir $(OPM)) ;\
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\ curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.55.0/$${OS}-$${ARCH}-opm ;\
chmod +x $(OPM) ;\ chmod +x $(OPM) ;\
} }
else else
@@ -285,7 +369,7 @@ endif
# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
.PHONY: catalog-build .PHONY: catalog-build
catalog-build: opm ## Build a catalog image. catalog-build: opm ## Build a catalog image.
$(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) $(OPM) index add --container-tool $(CONTAINER_TOOL) --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
# Push the catalog image. # Push the catalog image.
.PHONY: catalog-push .PHONY: catalog-push

View File

@@ -17,25 +17,31 @@ The 1Password Connect Kubernetes Operator provides the ability to integrate Kube
### 🚀 Quickstart ### 🚀 Quickstart
1. Add the [1Passsword Helm Chart](https://github.com/1Password/connect-helm-charts) to your repository. 1. Add the [1Password Helm Chart](https://github.com/1Password/connect-helm-charts) to your repository.
2. Run the following command to install Connect and the 1Password Kubernetes Operator in your infrastructure: 2. Run the following command to install Connect and the 1Password Kubernetes Operator in your infrastructure:
``` ```
helm install connect 1password/connect --set-file connect.credentials=1password-credentials-demo.json --set operator.create=true --set operator.token.value = <your connect token> helm install connect 1password/connect --set-file connect.credentials=1password-credentials-demo.json --set operator.create=true --set operator.token.value = <your connect token>
``` ```
3. Create a Kubernetes Secret from a 1Password item: 3. Create a Kubernetes Secret from a 1Password item:
```apiVersion: onepassword.com/v1
```
apiVersion: onepassword.com/v1
kind: OnePasswordItem kind: OnePasswordItem
metadata: metadata:
name: <item_name> #this name will also be used for naming the generated kubernetes secret name: <item_name> #this name will also be used for naming the generated kubernetes secret
spec: spec:
itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>" itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
``` ```
Deploy the OnePasswordItem to Kubernetes: Deploy the OnePasswordItem to Kubernetes:
``` ```
kubectl apply -f <your_item>.yaml kubectl apply -f <your_item>.yaml
``` ```
Check that the Kubernetes Secret has been generated: Check that the Kubernetes Secret has been generated:
``` ```
@@ -43,6 +49,7 @@ kubectl get secret <secret_name>
``` ```
### 📄 Usage ### 📄 Usage
Refer to the [Usage Guide](USAGEGUIDE.md) for documentation on how to deploy and use the 1Password Operator. Refer to the [Usage Guide](USAGEGUIDE.md) for documentation on how to deploy and use the 1Password Operator.
## 💙 Community & Support ## 💙 Community & Support
@@ -55,6 +62,4 @@ Refer to the [Usage Guide](USAGEGUIDE.md) for documentation on how to deploy and
1Password requests you practice responsible disclosure if you discover a vulnerability. 1Password requests you practice responsible disclosure if you discover a vulnerability.
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits). Please file requests by sending an email to bugbounty@agilebits.com.
For information about security practices, please visit the [1Password Bug Bounty Program](https://bugcrowd.com/agilebits).

View File

@@ -5,107 +5,53 @@
## Table of Contents ## Table of Contents
- [Prerequisites](#prerequisites) 1. [Configuration Options](#configuration-options)
- [Deploying 1Password Connect to Kubernetes](#deploying-1password-connect-to-kubernetes) 2. [Use Kubernetes Operator with Service Account](#use-kubernetes-operator-with-service-account)
- [Kubernetes Operator Deployment](#kubernetes-operator-deployment) - [Create a Service Account](#1-create-a-service-account)
- [Usage](#usage) - [Create a Kubernetes secret](#2-create-a-kubernetes-secret-for-the-service-account)
- [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments) - [Deploy the Operator](#3-deploy-the-operator)
- [Development](#development) 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)
## Prerequisites
- [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/)
- [A `1password-credentials.json` file generated and a 1Password Connect API Token issued for the K8s Operator integration](https://developer.1password.com/docs/connect/get-started/#step-1-set-up-a-secrets-automation-workflow)
## Deploying 1Password Connect to Kubernetes ## 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/)
If 1Password Connect is already running, you can skip this step. ---
There are options to deploy 1Password Connect: ## Use Kubernetes Operator with Service Account
- [Deploy with Helm](#deploy-with-helm) ### 1. [Create a service account](https://developer.1password.com/docs/service-accounts/get-started#create-a-service-account)
- [Deploy using the Connect Operator](#deploy-using-the-connect-operator) ### 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.
### Deploy with Helm - Create Kubernetes secret:
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
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 ```bash
cat 1password-credentials.json | base64 | \ kubectl create secret generic onepassword-service-account-token --from-literal=token="$OP_SERVICE_ACCOUNT_TOKEN"
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
``` ```
Create a Kubernetes secret from the op-session file: ### 3. Deploy the Operator
```bash
kubectl create secret generic op-credentials --from-file=op-session
```
Add the following environment variable to the onepassword-connect-operator container in `/config/manager/manager.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 current 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>)
```
**Deploying the Operator**
An sample Deployment yaml can be found at `/config/manager/manager.yaml`. 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: 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. - **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. - **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. - **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password.
- **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. 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.
- **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.
You can also set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. (Note: the default value is `debug`.)
Example:
```yaml
.
.
.
containers:
- command:
- /manager
args:
- --leader-elect
- --zap-log-level=info
image: 1password/onepassword-operator:latest
.
.
.
```
To deploy the operator, simply run the following command: To deploy the operator, simply run the following command:
```shell ```shell
@@ -118,59 +64,57 @@ make deploy
make undeploy make undeploy
``` ```
## Usage ---
To create a Kubernetes Secret from a 1Password item, create a yaml file with the following ## 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 ```yaml
apiVersion: onepassword.com/v1 ....
kind: OnePasswordItem containers:
metadata: - command:
name: <item_name> #this name will also be used for naming the generated kubernetes secret - /manager
spec: args:
itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>" - --leader-elect
- --zap-log-level=info
image: 1password/onepassword-operator:latest
....
``` ```
Deploy the OnePasswordItem to Kubernetes: ---
```bash ## Usage examples
kubectl apply -f <your_item>.yaml Find usage [examples](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#usage-examples) on 1Password developer documentation.
```
To test that the Kubernetes Secret check that the following command returns a secret: ---
```bash ## How 1Password Items Map to Kubernetes Secrets
kubectl get secret <secret_name>
```
**Note:** Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.
To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata:
```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>"
```
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.
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. 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. 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. Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence.
**Note:** Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret. 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 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. 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.
@@ -237,6 +181,8 @@ metadata:
If the value is not set, the auto restart settings on the deployment will be used. If the value is not set, the auto restart settings on the deployment will be used.
---
## Development ## Development
### How it works ### How it works

View File

@@ -25,18 +25,19 @@ SOFTWARE.
package main package main
import ( import (
"context"
"crypto/tls"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/1Password/connect-sdk-go/connect"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them. // to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -47,13 +48,17 @@ import (
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache" "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/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap" "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" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1" onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/internal/controller" "github.com/1Password/onepassword-operator/internal/controller"
op "github.com/1Password/onepassword-operator/pkg/onepassword" 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/pkg/utils"
"github.com/1Password/onepassword-operator/version" "github.com/1Password/onepassword-operator/version"
// +kubebuilder:scaffold:imports // +kubebuilder:scaffold:imports
@@ -73,13 +78,6 @@ const (
annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
) )
// Change below variables to serve metrics on different host or port.
var (
metricsHost = "0.0.0.0"
metricsPort int32 = 8383
operatorMetricsPort int32 = 8686
)
func printVersion() { func printVersion() {
setupLog.Info(fmt.Sprintf("Operator Version: %s", version.OperatorVersion)) setupLog.Info(fmt.Sprintf("Operator Version: %s", version.OperatorVersion))
setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
@@ -96,13 +94,31 @@ func init() {
func main() { func main() {
var metricsAddr string var metricsAddr string
var metricsCertPath, metricsCertName, metricsCertKey string
var webhookCertPath, webhookCertName, webhookCertKey string
var enableLeaderElection bool var enableLeaderElection bool
var probeAddr string var probeAddr string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") var secureMetrics bool
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 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, flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+ "Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active 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{ opts := zap.Options{
Development: true, Development: true,
} }
@@ -111,8 +127,26 @@ func main() {
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 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() printVersion()
// Create a root context that will be cancelled on termination signals
ctx := ctrl.SetupSignalHandler()
watchNamespace, err := getWatchNamespace() watchNamespace, err := getWatchNamespace()
if err != nil { if err != nil {
setupLog.Error(err, "unable to get WatchNamespace, "+ setupLog.Error(err, "unable to get WatchNamespace, "+
@@ -125,12 +159,104 @@ func main() {
os.Exit(1) 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{ options := ctrl.Options{
Scheme: scheme, Scheme: scheme,
Metrics: metricsserver.Options{BindAddress: metricsAddr}, Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr, HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection, LeaderElection: enableLeaderElection,
LeaderElectionID: "c26807fd.onepassword.com", 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) // Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
@@ -153,16 +279,19 @@ func main() {
} }
// Setup One Password Client // Setup One Password Client
opConnectClient, err := connect.NewClientFromEnvironment() opClient, err := opclient.NewFromEnvironment(ctx, opclient.Config{
Logger: setupLog,
Version: version.OperatorVersion,
})
if err != nil { if err != nil {
setupLog.Error(err, "unable to create Connect client") setupLog.Error(err, "unable to create 1Password client")
os.Exit(1) os.Exit(1)
} }
if err = (&controller.OnePasswordItemReconciler{ if err = (&controller.OnePasswordItemReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
OpConnectClient: opConnectClient, OpClient: opClient,
}).SetupWithManager(mgr); err != nil { }).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem") setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem")
os.Exit(1) os.Exit(1)
@@ -172,7 +301,7 @@ func main() {
if err = (&controller.DeploymentReconciler{ if err = (&controller.DeploymentReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
OpConnectClient: opConnectClient, OpClient: opClient,
OpAnnotationRegExp: r, OpAnnotationRegExp: r,
}).SetupWithManager(mgr); err != nil { }).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Deployment") setupLog.Error(err, "unable to create controller", "controller", "Deployment")
@@ -183,10 +312,10 @@ func main() {
// Setup 1PasswordConnect // Setup 1PasswordConnect
if shouldManageConnect() { if shouldManageConnect() {
setupLog.Info("Automated Connect Management Enabled") setupLog.Info("Automated Connect Management Enabled")
go func() { go func(ctx context.Context) {
connectStarted := false connectStarted := false
for connectStarted == false { for !connectStarted {
err := op.SetupConnect(mgr.GetClient(), deploymentNamespace) err := op.SetupConnect(ctx, mgr.GetClient(), deploymentNamespace)
// Cache Not Started is an acceptable error. Retry until cache is started. // Cache Not Started is an acceptable error. Retry until cache is started.
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
setupLog.Error(err, "") setupLog.Error(err, "")
@@ -196,29 +325,37 @@ func main() {
connectStarted = true connectStarted = true
} }
} }
}() }(ctx)
} else { } else {
setupLog.Info("Automated Connect Management Disabled") setupLog.Info("Automated Connect Management Disabled")
} }
// Setup update secrets task // Setup update secrets task
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments()) updatedSecretsPoller := op.NewManager(mgr.GetClient(), opClient, shouldAutoRestartDeployments())
done := make(chan bool) done := make(chan bool)
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets()) ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
go func() { go func(ctx context.Context) {
for { for {
select { select {
case <-done: case <-done:
ticker.Stop() ticker.Stop()
return return
case <-ticker.C: case <-ticker.C:
err := updatedSecretsPoller.UpdateKubernetesSecretsTask() err := updatedSecretsPoller.UpdateKubernetesSecretsTask(ctx)
if err != nil { if err != nil {
setupLog.Error(err, "error running update kubernetes secret task") 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 { if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check") setupLog.Error(err, "unable to set up health check")
@@ -230,7 +367,7 @@ func main() {
} }
setupLog.Info("starting manager") setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { if err := mgr.Start(ctx); err != nil {
setupLog.Error(err, "problem running manager") setupLog.Error(err, "problem running manager")
os.Exit(1) os.Exit(1)
} }

View File

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

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.13.0 controller-gen.kubebuilder.io/version: v0.18.0
name: onepassworditems.onepassword.com name: onepassworditems.onepassword.com
spec: spec:
group: onepassword.com group: onepassword.com
@@ -20,14 +20,19 @@ spec:
description: OnePasswordItem is the Schema for the onepassworditems API description: OnePasswordItem is the Schema for the onepassworditems API
properties: properties:
apiVersion: apiVersion:
description: 'APIVersion defines the versioned schema of this representation description: |-
of an object. Servers should convert recognized schemas to the latest APIVersion defines the versioned schema of this representation of an object.
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 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 type: string
kind: kind:
description: 'Kind is a string value representing the REST resource this description: |-
object represents. Servers may infer this from the endpoint the client Kind is a string value representing the REST resource this object represents.
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 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 type: string
metadata: metadata:
type: object type: object

View File

@@ -11,13 +11,7 @@ patches:
#- path: patches/webhook_in_onepassworditems.yaml #- path: patches/webhook_in_onepassworditems.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch #+kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
# patches here are for enabling the CA injection for each CRD
#- path: patches/cainjection_in_onepassworditems.yaml
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
# [WEBHOOK] To enable webhook, uncomment the following section # [WEBHOOK] To enable webhook, uncomment the following section
# the following config is for teaching kustomize how to do kustomization for CRDs. # the following config is for teaching kustomize how to do kustomization for CRDs.
#configurations: #configurations:
#- kustomizeconfig.yaml #- kustomizeconfig.yaml

View File

@@ -1,7 +0,0 @@
# The following patch adds a directive for certmanager to inject CA into the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
name: onepassworditems.onepassword.com

View File

@@ -1,16 +0,0 @@
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: onepassworditems.onepassword.com
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1

View File

@@ -25,31 +25,141 @@ resources:
#- ../certmanager #- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../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: patches:
# Protect the /metrics endpoint by putting it behind auth. # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
# If you want your controller-manager to expose the /metrics # More info: https://book.kubebuilder.io/reference/metrics
# endpoint w/o any authn/z, please comment the following line. - path: manager_metrics_patch.yaml
- path: manager_auth_proxy_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 # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml # crd/kustomization.yaml
#- path: manager_webhook_patch.yaml #- path: manager_webhook_patch.yaml
# target:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # kind: Deployment
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
#- path: webhookcainjection_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations # Uncomment the following replacements to add the cert-manager CA injection annotations
#replacements: #replacements:
# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs # - 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 # kind: Certificate
# group: cert-manager.io # group: cert-manager.io
# version: v1 # version: v1
# name: serving-cert # this name should match the one in certificate.yaml # name: metrics-certs
# fieldPath: .metadata.namespace # namespace of the certificate CR # 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: # targets:
# - select: # - select:
# kind: ValidatingWebhookConfiguration # kind: ValidatingWebhookConfiguration
@@ -59,27 +169,11 @@ patches:
# delimiter: '/' # delimiter: '/'
# index: 0 # index: 0
# create: true # create: true
# - select:
# kind: MutatingWebhookConfiguration
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - select:
# kind: CustomResourceDefinition
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 0
# create: true
# - source: # - source:
# kind: Certificate # kind: Certificate
# group: cert-manager.io # group: cert-manager.io
# version: v1 # version: v1
# name: serving-cert # this name should match the one in certificate.yaml # name: serving-cert
# fieldPath: .metadata.name # fieldPath: .metadata.name
# targets: # targets:
# - select: # - select:
@@ -90,6 +184,29 @@ patches:
# delimiter: '/' # delimiter: '/'
# index: 1 # index: 1
# create: true # 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: # - select:
# kind: MutatingWebhookConfiguration # kind: MutatingWebhookConfiguration
# fieldPaths: # fieldPaths:
@@ -98,45 +215,20 @@ patches:
# delimiter: '/' # delimiter: '/'
# index: 1 # index: 1
# create: true # create: true
# - select: #
# kind: CustomResourceDefinition # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
# fieldPaths:
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
# options:
# delimiter: '/'
# index: 1
# create: true
# - source: # Add cert-manager annotation to the webhook Service
# kind: Service
# version: v1
# name: webhook-service
# fieldPath: .metadata.name # namespace of the service
# targets:
# - select:
# kind: Certificate # kind: Certificate
# group: cert-manager.io # group: cert-manager.io
# version: v1 # version: v1
# fieldPaths: # name: serving-cert
# - .spec.dnsNames.0 # fieldPath: .metadata.namespace # Namespace of the certificate CR
# - .spec.dnsNames.1 # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
# options: # +kubebuilder:scaffold:crdkustomizecainjectionns
# delimiter: '.'
# index: 0
# create: true
# - source: # - source:
# kind: Service
# version: v1
# name: webhook-service
# fieldPath: .metadata.namespace # namespace of the service
# targets:
# - select:
# kind: Certificate # kind: Certificate
# group: cert-manager.io # group: cert-manager.io
# version: v1 # version: v1
# fieldPaths: # name: serving-cert
# - .spec.dnsNames.0 # fieldPath: .metadata.name
# - .spec.dnsNames.1 # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
# options: # +kubebuilder:scaffold:crdkustomizecainjectionname
# delimiter: '.'
# index: 1
# create: true

View File

@@ -1,41 +0,0 @@
# This patch inject a sidecar container which is a HTTP proxy for the
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
apiVersion: apps/v1
kind: Deployment
metadata:
name: onepassword-connect-operator
namespace: system
spec:
template:
spec:
securityContext:
runAsNonRoot: true
containers:
- name: kube-rbac-proxy
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0
args:
- "--secure-listen-address=0.0.0.0:8443"
- "--upstream=http://127.0.0.1:8080/"
- "--logtostderr=true"
- "--v=0"
ports:
- containerPort: 8443
protocol: TCP
name: https
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 5m
memory: 64Mi
- name: manager
args:
- "--health-probe-bind-address=:8081"
- "--metrics-bind-address=127.0.0.1:8080"
- "--leader-elect"

View File

@@ -1,10 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: onepassword-connect-operator
namespace: system
spec:
template:
spec:
containers:
- name: manager

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

@@ -72,28 +72,37 @@ spec:
- /manager - /manager
args: args:
- --leader-elect - --leader-elect
- --health-probe-bind-address=:8081
image: 1password/onepassword-operator:latest image: 1password/onepassword-operator:latest
name: manager name: manager
env: env:
- name: WATCH_NAMESPACE - name: OPERATOR_NAME
value: "default" value: "onepassword-connect-operator"
- name: POD_NAME - name: POD_NAME
valueFrom: valueFrom:
fieldRef: fieldRef:
fieldPath: metadata.name fieldPath: metadata.name
- name: OPERATOR_NAME - name: WATCH_NAMESPACE
value: "onepassword-connect-operator" value: "default"
- name: OP_CONNECT_HOST
value: "http://onepassword-connect:8080"
- name: POLLING_INTERVAL - name: POLLING_INTERVAL
value: "10" value: "10"
- name: AUTO_RESTART
value: "false"
- name: OP_CONNECT_HOST
value: "http://onepassword-connect:8080"
- name: OP_CONNECT_TOKEN - name: OP_CONNECT_TOKEN
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: onepassword-token name: onepassword-token
key: token key: token
- name: AUTO_RESTART - name: MANAGE_CONNECT
value: "false" value: "false"
# 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: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
@@ -116,9 +125,9 @@ spec:
resources: resources:
limits: limits:
cpu: 500m cpu: 500m
memory: 128Mi memory: 512Mi
requests: requests:
cpu: 10m cpu: 100m
memory: 64Mi memory: 128Mi
serviceAccountName: onepassword-connect-operator serviceAccountName: onepassword-connect-operator
terminationGracePeriodSeconds: 10 terminationGracePeriodSeconds: 10

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

@@ -1,2 +1,11 @@
resources: resources:
- monitor.yaml - 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

@@ -5,7 +5,7 @@ metadata:
labels: labels:
name: onepassword-connect-operator name: onepassword-connect-operator
control-plane: onepassword-connect-operator control-plane: onepassword-connect-operator
app.kubernetes.io/name: servicemonitor app.kubernetes.io/name: onepassword-operator
app.kubernetes.io/instance: controller-manager-metrics-monitor app.kubernetes.io/instance: controller-manager-metrics-monitor
app.kubernetes.io/component: metrics app.kubernetes.io/component: metrics
app.kubernetes.io/created-by: onepassword-connect-operator app.kubernetes.io/created-by: onepassword-connect-operator
@@ -16,12 +16,22 @@ metadata:
spec: spec:
endpoints: endpoints:
- path: /metrics - path: /metrics
port: https port: https # Ensure this is the name of the port that exposes HTTPS metrics
scheme: https scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig: 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 insecureSkipVerify: true
selector: selector:
matchLabels: matchLabels:
name: onepassword-connect-operator name: onepassword-connect-operator
control-plane: 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

@@ -1,16 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: clusterrole
app.kubernetes.io/instance: metrics-reader
app.kubernetes.io/component: kube-rbac-proxy
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: metrics-reader
rules:
- nonResourceURLs:
- "/metrics"
verbs:
- get

View File

@@ -1,24 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: clusterrole
app.kubernetes.io/instance: proxy-role
app.kubernetes.io/component: kube-rbac-proxy
app.kubernetes.io/created-by: onepassword-connect-operator
app.kubernetes.io/part-of: onepassword-connect-operator
app.kubernetes.io/managed-by: kustomize
name: proxy-role
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create

View File

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

View File

@@ -1,23 +0,0 @@
apiVersion: v1
kind: Service
metadata:
labels:
name: onepassword-connect-operator
control-plane: onepassword-connect-operator
app.kubernetes.io/name: service
app.kubernetes.io/instance: controller-manager-metrics-service
app.kubernetes.io/component: kube-rbac-proxy
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-service
namespace: system
spec:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: https
selector:
name: onepassword-connect-operator
control-plane: onepassword-connect-operator

View File

@@ -9,10 +9,19 @@ resources:
- role_binding.yaml - role_binding.yaml
- leader_election_role.yaml - leader_election_role.yaml
- leader_election_role_binding.yaml - leader_election_role_binding.yaml
# Comment the following 4 lines if you want to disable # The following RBAC configurations are used to protect
# the auth proxy (https://github.com/brancz/kube-rbac-proxy) # the metrics endpoint with authn/authz. These configurations
# which protects your /metrics endpoint. # ensure that only authorized users and service accounts
- auth_proxy_service.yaml # can access the metrics endpoint. Comment the following
- auth_proxy_role.yaml # permissions if you want to disable this protection.
- auth_proxy_role_binding.yaml # More info: https://book.kubebuilder.io/reference/metrics.html
- auth_proxy_client_clusterrole.yaml - 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,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

@@ -1,4 +1,10 @@
# permissions for end users to edit onepassworditems. # 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 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole
metadata: metadata:

View File

@@ -1,4 +1,10 @@
# permissions for end users to view onepassworditems. # 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 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole
metadata: metadata:

View File

@@ -24,12 +24,6 @@ rules:
- patch - patch
- update - update
- watch - watch
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups: - apiGroups:
- apps - apps
resources: resources:
@@ -45,25 +39,6 @@ rules:
- patch - patch
- update - update
- watch - watch
- apiGroups:
- apps
resources:
- deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
- deployments
- replicasets
verbs:
- get
- apiGroups: - apiGroups:
- apps - apps
resources: resources:
@@ -106,17 +81,6 @@ rules:
- onepassword.com - onepassword.com
resources: resources:
- '*' - '*'
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- onepassword.com
resources:
- onepassworditems - onepassworditems
verbs: verbs:
- create - create

109
docs/fork-pr-testing.md Normal file
View File

@@ -0,0 +1,109 @@
# Fork PR Testing Guide
This document explains how to test external pull requests using the dispatch action workflow system.
## Overview
The testing system consists of three main workflows:
1. **E2E Tests [triggered by maintainer]** (`test-e2e.yml`) - Runs automatically for internal PRs, requires approval for external PRs
2. **Ok To Test** (`ok-to-test.yml`) - Processes slash commands to trigger fork testing
3. **E2E tests [fork]** (`test-e2e-fork.yml`) - Triggered manually via slash commands for external PRs
## How It Works
### 1. Initial PR State
When a PR is created or updated:
- The `test-e2e.yml` workflow automatically runs
- For **external PRs**: The `check-external-pr` job detects it's from a fork and **fails intentionally**
- For **internal PRs**: The workflow proceeds normally with e2e tests
- External PRs show ❌ for the check-external-pr job with a message asking for maintainer approval
### 2. Manual Approval Required
**Important**: External PRs require manual approval before workflows can run.
**Steps for maintainers:**
1. Go to the PR page
2. Click **"Approve workflow to run"** button
3. The workflow will then execute
**Note**: The `test-e2e.yml` workflow will still fail for external PRs even after approval, as it's designed to prevent automatic execution. Need to use the slash command instead.
### 3. Testing External PRs
Once the initial checks have run (and failed), maintainers can test the PR using slash commands:
#### Step-by-Step Process:
1. **Navigate to the PR**
- Go to the pull request you want to test
2. **Add a comment with slash command**
```
/ok-to-test sha=<commit-sha>
```
Replace `<commit-sha>` with the actual commit SHA from the PR.
**Note**: Use the short SHA.
3. **Dispatch Action Triggers**
- The `ok-to-test.yml` workflow processes the slash command
- It triggers a `repository_dispatch` event with type `ok-to-test-command`
- The `test-e2e-fork.yml` workflow starts
4. **Workflow Execution**
The fork workflow runs two jobs:
**a) `e2e-tests` job** (conditional)
- Only runs if:
- Event is `repository_dispatch`
- SHA parameter is not empty
- PR head SHA contains the provided SHA
- Calls the reusable `e2e-tests.yml` workflow
- Runs the actual E2E tests if conditions are met
**b) `update-check-status` job** (conditional)
- Runs after `e2e-tests` completes
- Updates the existing check for job named "e2e-tests"
- Sets the conclusion based on `e2e-tests` result:
- ✅ **Success** if tests pass
- ❌ **Failure** if tests fail
## Troubleshooting
### Workflow Not Running
- **Check**: Ensure you've approved the workflow to run
- **Action**: Click "Approve workflow to run" in the PR checks tab
### SHA Not Found
- **Check**: Verify the SHA exists in the PR commits
- **Action**: Use `git log --oneline` to find valid commit SHAs
### Dispatch Not Triggering
- **Check**: Ensure the slash command format is correct
- **Action**: Use exact format: `/ok-to-test sha=<sha>`
### Check Runs Not Updating
- **Note**: The fork workflow updates the existing "E2E Tests [reusable]" check run
- **Behavior**: The same check run is updated with new results rather than creating a new one
## Security Notes
- Only users with **write** permissions can trigger dispatch commands
- The fork workflow runs in the main repository context, allowing it to update check runs
- Manual approval is required for external PR workflows
- External PRs are automatically detected and prevented from running tests automatically
## Workflow Files
- **`.github/workflows/ok-to-test.yml`** - Ok To Test - Slash command processor
- **`.github/workflows/test-e2e.yml`** - E2E Tests [triggered by maintainer] - Initial PR checks
- **`.github/workflows/test-e2e-fork.yml`** - E2E tests [fork] - Dispatch action handler
- **`.github/workflows/e2e-tests.yml`** - E2E Tests [reusable] - Reusable workflow for actual testing
## Testing Checklist
- [ ] PR created with failing check-external-pr check (for external PRs)
- [ ] Workflow approved to run (if required)
- [ ] Slash command posted with valid SHA
- [ ] Dispatch action triggered successfully
- [ ] `check-external-pr` check run updated with correct conclusion

22
docs/testing.md Normal file
View File

@@ -0,0 +1,22 @@
# Testing
## Unit & Integration tests
**When**: Unit (pure Go) and integration (controller-runtime envtest).
**Where**: `internal/...`, `pkg/...`
**Add files in**: `*_test.go` next to the code.
**Run**: `make test`
## E2E tests (kind)
**When**: Full cluster behavior (CRDs, operator image, Connect/SA flows).
**Where**: `test/e2e/...`
**Add files in**: `*_test.go` next to the code.
**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`

142
go.mod
View File

@@ -1,84 +1,116 @@
module github.com/1Password/onepassword-operator module github.com/1Password/onepassword-operator
go 1.21 go 1.24.0
toolchain go1.21.5 toolchain go1.24.5
require ( require (
github.com/1Password/connect-sdk-go v1.5.3 github.com/1Password/connect-sdk-go v1.5.3
github.com/onsi/ginkgo/v2 v2.13.2 github.com/1password/onepassword-sdk-go v0.3.1
github.com/onsi/gomega v1.30.0 github.com/go-logr/logr v1.4.2
github.com/stretchr/testify v1.8.4 github.com/onsi/ginkgo/v2 v2.22.0
k8s.io/api v0.29.0 github.com/onsi/gomega v1.36.1
k8s.io/apimachinery v0.29.0 github.com/stretchr/testify v1.10.0
k8s.io/client-go v0.29.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 k8s.io/kubectl v0.29.0
sigs.k8s.io/controller-runtime v0.16.3 sigs.k8s.io/controller-runtime v0.21.0
) )
require ( 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/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.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.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/extism/go-sdk v1.7.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/zapr v1.2.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // 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/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.3 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/google/cel-go v0.23.2 // indirect
github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/imdario/mergo v0.3.6 // 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/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.10.1 // 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/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-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
go.uber.org/atomic v1.10.0 // 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/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.17.0 // indirect golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.14.0 // indirect golang.org/x/sync v0.15.0 // indirect
golang.org/x/term v0.13.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/term v0.32.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.14.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 gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/protobuf v1.31.0 // 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/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.28.3 // indirect k8s.io/apiserver v0.33.0 // indirect
k8s.io/component-base v0.29.0 // indirect k8s.io/component-base v0.33.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/yaml v1.3.0 // 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
) )

339
go.sum
View File

@@ -1,86 +1,101 @@
cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk= github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk=
github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs= github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs=
github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXhyj4NBmo5Tdamc=
github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/extism/go-sdk v1.7.0 h1:yHbSa2JbcF60kjGsYiGEOcClfbknqCJchyh9TRibFWo=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/extism/go-sdk v1.7.0/go.mod h1:Dhuc1qcD0aqjdqJ3ZDyGdkZPEj/EHKVjbE4P+1XRMqc=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -88,162 +103,176 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc=
k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8=
k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI= k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI=
k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs= k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@@ -28,14 +28,13 @@ import (
"context" "context"
"fmt" "fmt"
"regexp" "regexp"
"strings"
"sigs.k8s.io/controller-runtime/pkg/reconcile" "time"
"github.com/1Password/connect-sdk-go/connect"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs" "github.com/1Password/onepassword-operator/pkg/logs"
op "github.com/1Password/onepassword-operator/pkg/onepassword" 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/pkg/utils"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
@@ -47,6 +46,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
) )
var logDeployment = logf.Log.WithName("controller_deployment") var logDeployment = logf.Log.WithName("controller_deployment")
@@ -55,7 +55,7 @@ var logDeployment = logf.Log.WithName("controller_deployment")
type DeploymentReconciler struct { type DeploymentReconciler struct {
client.Client client.Client
Scheme *runtime.Scheme Scheme *runtime.Scheme
OpConnectClient connect.Client OpClient opclient.Client
OpAnnotationRegExp *regexp.Regexp OpAnnotationRegExp *regexp.Regexp
} }
@@ -77,7 +77,7 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
reqLogger.V(logs.DebugLevel).Info("Reconciling Deployment") reqLogger.V(logs.DebugLevel).Info("Reconciling Deployment")
deployment := &appsv1.Deployment{} deployment := &appsv1.Deployment{}
err := r.Get(context.Background(), req.NamespacedName, deployment) err := r.Get(ctx, req.NamespacedName, deployment)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
return reconcile.Result{}, nil return reconcile.Result{}, nil
@@ -92,32 +92,37 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
} }
// If the deployment is not being deleted // If the deployment is not being deleted
if deployment.ObjectMeta.DeletionTimestamp.IsZero() { if deployment.DeletionTimestamp.IsZero() {
// Adds a finalizer to the deployment if one does not exist. // Adds a finalizer to the deployment if one does not exist.
// This is so we can handle cleanup of associated secrets properly // This is so we can handle cleanup of associated secrets properly
if !utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) { if !utils.ContainsString(deployment.Finalizers, finalizer) {
deployment.ObjectMeta.Finalizers = append(deployment.ObjectMeta.Finalizers, finalizer) deployment.Finalizers = append(deployment.Finalizers, finalizer)
if err = r.Update(context.Background(), deployment); err != nil { if err = r.Update(ctx, deployment); err != nil {
return reconcile.Result{}, err return reconcile.Result{}, err
} }
} }
// Handles creation or updating secrets for deployment if needed // Handles creation or updating secrets for deployment if needed
if err = r.handleApplyingDeployment(deployment, deployment.Namespace, annotations, req); err != nil { 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{}, err
} }
}
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
// The deployment has been marked for deletion. If the one password // The deployment has been marked for deletion. If the one password
// finalizer is found there are cleanup tasks to perform // finalizer is found there are cleanup tasks to perform
if utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) { if utils.ContainsString(deployment.Finalizers, finalizer) {
secretName := annotations[op.NameAnnotation] secretName := annotations[op.NameAnnotation]
if err = r.cleanupKubernetesSecretForDeployment(secretName, deployment); err != nil { if err = r.cleanupKubernetesSecretForDeployment(ctx, secretName, deployment); err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Remove the finalizer from the deployment so deletion of deployment can be completed // Remove the finalizer from the deployment so deletion of deployment can be completed
if err = r.removeOnePasswordFinalizerFromDeployment(deployment); err != nil { if err = r.removeOnePasswordFinalizerFromDeployment(ctx, deployment); err != nil {
return reconcile.Result{}, err return reconcile.Result{}, err
} }
} }
@@ -128,27 +133,28 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr). return ctrl.NewControllerManagedBy(mgr).
For(&appsv1.Deployment{}). For(&appsv1.Deployment{}).
Named("onepassword-deployment").
Complete(r) Complete(r)
} }
func (r *DeploymentReconciler) cleanupKubernetesSecretForDeployment(secretName string, deletedDeployment *appsv1.Deployment) error { func (r *DeploymentReconciler) cleanupKubernetesSecretForDeployment(ctx context.Context, secretName string, deletedDeployment *appsv1.Deployment) error {
kubernetesSecret := &corev1.Secret{} kubernetesSecret := &corev1.Secret{}
kubernetesSecret.ObjectMeta.Name = secretName kubernetesSecret.Name = secretName
kubernetesSecret.ObjectMeta.Namespace = deletedDeployment.Namespace kubernetesSecret.Namespace = deletedDeployment.Namespace
if len(secretName) == 0 { if len(secretName) == 0 {
return nil return nil
} }
updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret} updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret}
multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment) multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(ctx, updatedSecrets, *deletedDeployment)
if err != nil { if err != nil {
return err return err
} }
// Only delete the associated kubernetes secret if it is not being used by other deployments // Only delete the associated kubernetes secret if it is not being used by other deployments
if !multipleDeploymentsUsingSecret { if !multipleDeploymentsUsingSecret {
if err = r.Delete(context.Background(), kubernetesSecret); err != nil { if err = r.Delete(ctx, kubernetesSecret); err != nil {
if !errors.IsNotFound(err) { if !errors.IsNotFound(err) {
return err return err
} }
@@ -157,13 +163,13 @@ func (r *DeploymentReconciler) cleanupKubernetesSecretForDeployment(secretName s
return nil return nil
} }
func (r *DeploymentReconciler) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) { func (r *DeploymentReconciler) areMultipleDeploymentsUsingSecret(ctx context.Context, updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) {
deployments := &appsv1.DeploymentList{} deployments := &appsv1.DeploymentList{}
opts := []client.ListOption{ opts := []client.ListOption{
client.InNamespace(deletedDeployment.Namespace), client.InNamespace(deletedDeployment.Namespace),
} }
err := r.List(context.Background(), deployments, opts...) err := r.List(ctx, deployments, opts...)
if err != nil { if err != nil {
logDeployment.Error(err, "Failed to list kubernetes deployments") logDeployment.Error(err, "Failed to list kubernetes deployments")
return false, err return false, err
@@ -179,12 +185,12 @@ func (r *DeploymentReconciler) areMultipleDeploymentsUsingSecret(updatedSecrets
return false, nil return false, nil
} }
func (r *DeploymentReconciler) removeOnePasswordFinalizerFromDeployment(deployment *appsv1.Deployment) error { func (r *DeploymentReconciler) removeOnePasswordFinalizerFromDeployment(ctx context.Context, deployment *appsv1.Deployment) error {
deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer) deployment.Finalizers = utils.RemoveString(deployment.Finalizers, finalizer)
return r.Update(context.Background(), deployment) return r.Update(ctx, deployment)
} }
func (r *DeploymentReconciler) handleApplyingDeployment(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error { 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) reqLog := logDeployment.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
secretName := annotations[op.NameAnnotation] secretName := annotations[op.NameAnnotation]
@@ -196,15 +202,15 @@ func (r *DeploymentReconciler) handleApplyingDeployment(deployment *appsv1.Deplo
return nil return nil
} }
item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, annotations[op.ItemPathAnnotation]) item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, annotations[op.ItemPathAnnotation])
if err != nil { if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err) return fmt.Errorf("failed to retrieve item: %w", err)
} }
// Create owner reference. // Create owner reference.
gvk, err := apiutil.GVKForObject(deployment, r.Scheme) gvk, err := apiutil.GVKForObject(deployment, r.Scheme)
if err != nil { if err != nil {
return fmt.Errorf("could not to retrieve group version kind: %v", err) return fmt.Errorf("could not to retrieve group version kind: %w", err)
} }
ownerRef := &metav1.OwnerReference{ ownerRef := &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(), APIVersion: gvk.GroupVersion().String(),
@@ -213,5 +219,5 @@ func (r *DeploymentReconciler) handleApplyingDeployment(deployment *appsv1.Deplo
UID: deployment.GetUID(), UID: deployment.GetUID(),
} }
return kubeSecrets.CreateKubernetesSecretFromItem(r.Client, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef) return kubeSecrets.CreateKubernetesSecretFromItem(ctx, r.Client, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef)
} }

View File

@@ -2,9 +2,6 @@ package controller
import ( import (
"context" "context"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
"time" "time"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
@@ -17,6 +14,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
) )
const ( const (
@@ -26,14 +24,13 @@ const (
) )
var _ = Describe("Deployment controller", func() { var _ = Describe("Deployment controller", func() {
var ctx context.Context ctx := context.Background()
var deploymentKey types.NamespacedName var deploymentKey types.NamespacedName
var secretKey types.NamespacedName var secretKey types.NamespacedName
var deploymentResource *appsv1.Deployment var deploymentResource *appsv1.Deployment
createdSecret := &v1.Secret{} createdSecret := &v1.Secret{}
makeDeployment := func() { makeDeployment := func() {
ctx = context.Background()
deploymentKey = types.NamespacedName{ deploymentKey = types.NamespacedName{
Name: deploymentName, Name: deploymentName,
@@ -85,38 +82,26 @@ var _ = Describe("Deployment controller", func() {
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, secretKey, createdSecret) err := k8sClient.Get(ctx, secretKey, createdSecret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(createdSecret.Data).Should(Equal(item1.SecretData)) Expect(createdSecret.Data).Should(Equal(item1.SecretData))
} }
cleanK8sResources := func() { cleanK8sResources := func() {
// failed test runs that don't clean up leave resources behind. // failed test runs that don't clean up leave resources behind.
err := k8sClient.DeleteAllOf(context.Background(), &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace)) err := k8sClient.DeleteAllOf(ctx, &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace)) err = k8sClient.DeleteAllOf(ctx, &v1.Secret{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err = k8sClient.DeleteAllOf(context.Background(), &appsv1.Deployment{}, client.InNamespace(namespace)) err = k8sClient.DeleteAllOf(ctx, &appsv1.Deployment{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
} }
mockGetItemFunc := func() { mockGetItemFunc := func() {
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { // mock GetItemByID to return test item 'item1'
item := onepassword.Item{} mockGetItemByIDFunc.Return(item1.ToModel(), nil)
item.Fields = []*onepassword.ItemField{}
for k, v := range item1.Data {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = item1.Version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
} }
BeforeEach(func() { BeforeEach(func() {
@@ -151,17 +136,10 @@ var _ = Describe("Deployment controller", func() {
It("Should update existing K8s Secret using deployment", func() { It("Should update existing K8s Secret using deployment", func() {
By("Updating secret") By("Updating secret")
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{} // mock GetItemByID to return test item 'item2'
item.Fields = []*onepassword.ItemField{} mockGetItemByIDFunc.Return(item2.ToModel(), nil)
for k, v := range item2.Data {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = item2.Version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
Eventually(func() error { Eventually(func() error {
updatedDeployment := &appsv1.Deployment{ updatedDeployment := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
@@ -209,10 +187,7 @@ var _ = Describe("Deployment controller", func() {
updatedSecret := &v1.Secret{} updatedSecret := &v1.Secret{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, secretKey, updatedSecret) err := k8sClient.Get(ctx, secretKey, updatedSecret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(updatedSecret.Data).Should(Equal(item2.SecretData)) Expect(updatedSecret.Data).Should(Equal(item2.SecretData))
}) })
@@ -266,10 +241,7 @@ var _ = Describe("Deployment controller", func() {
updatedSecret := &v1.Secret{} updatedSecret := &v1.Secret{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, secretKey, updatedSecret) err := k8sClient.Get(ctx, secretKey, updatedSecret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(updatedSecret.Data).Should(Equal(item1.SecretData)) Expect(updatedSecret.Data).Should(Equal(item1.SecretData))
}) })

View File

@@ -27,13 +27,14 @@ package controller
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/1Password/connect-sdk-go/connect" "time"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs" "github.com/1Password/onepassword-operator/pkg/logs"
op "github.com/1Password/onepassword-operator/pkg/onepassword" 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/pkg/utils"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@@ -53,7 +54,7 @@ var finalizer = "onepassword.com/finalizer.secret"
type OnePasswordItemReconciler struct { type OnePasswordItemReconciler struct {
client.Client client.Client
Scheme *runtime.Scheme Scheme *runtime.Scheme
OpConnectClient connect.Client 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,verbs=get;list;watch;create;update;patch;delete
@@ -83,7 +84,7 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ
reqLogger.V(logs.DebugLevel).Info("Reconciling OnePasswordItem") reqLogger.V(logs.DebugLevel).Info("Reconciling OnePasswordItem")
onepassworditem := &onepasswordv1.OnePasswordItem{} onepassworditem := &onepasswordv1.OnePasswordItem{}
err := r.Get(context.Background(), req.NamespacedName, onepassworditem) err := r.Get(ctx, req.NamespacedName, onepassworditem)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
return ctrl.Result{}, nil return ctrl.Result{}, nil
@@ -92,33 +93,39 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ
} }
// If the deployment is not being deleted // If the deployment is not being deleted
if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() { if onepassworditem.DeletionTimestamp.IsZero() {
// Adds a finalizer to the deployment if one does not exist. // Adds a finalizer to the deployment if one does not exist.
// This is so we can handle cleanup of associated secrets properly // This is so we can handle cleanup of associated secrets properly
if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { if !utils.ContainsString(onepassworditem.Finalizers, finalizer) {
onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer) onepassworditem.Finalizers = append(onepassworditem.Finalizers, finalizer)
if err = r.Update(context.Background(), onepassworditem); err != nil { if err = r.Update(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
} }
// Handles creation or updating secrets for deployment if needed // Handles creation or updating secrets for deployment if needed
err = r.handleOnePasswordItem(onepassworditem, req) err = r.handleOnePasswordItem(ctx, onepassworditem, req)
if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil { 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{}, fmt.Errorf("cannot update status: %s", updateStatusErr)
} }
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// If one password finalizer exists then we must cleanup associated secrets // If one password finalizer exists then we must cleanup associated secrets
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { if utils.ContainsString(onepassworditem.Finalizers, finalizer) {
// Delete associated kubernetes secret // Delete associated kubernetes secret
if err = r.cleanupKubernetesSecret(onepassworditem); err != nil { if err = r.cleanupKubernetesSecret(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Remove finalizer now that cleanup is complete // Remove finalizer now that cleanup is complete
if err = r.removeFinalizer(onepassworditem); err != nil { if err = r.removeOnePasswordFinalizerFromOnePasswordItem(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
} }
@@ -129,23 +136,16 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ
func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr). return ctrl.NewControllerManagedBy(mgr).
For(&onepasswordv1.OnePasswordItem{}). For(&onepasswordv1.OnePasswordItem{}).
Named("onepassworditem").
Complete(r) Complete(r)
} }
func (r *OnePasswordItemReconciler) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error { func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer)
if err := r.Update(context.Background(), onePasswordItem); err != nil {
return err
}
return nil
}
func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error {
kubernetesSecret := &corev1.Secret{} kubernetesSecret := &corev1.Secret{}
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name kubernetesSecret.Name = onePasswordItem.Name
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace kubernetesSecret.Namespace = onePasswordItem.Namespace
if err := r.Delete(context.Background(), kubernetesSecret); err != nil { if err := r.Delete(ctx, kubernetesSecret); err != nil {
if !errors.IsNotFound(err) { if !errors.IsNotFound(err) {
return err return err
} }
@@ -153,26 +153,26 @@ func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *one
return nil return nil
} }
func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(opSecret *onepasswordv1.OnePasswordItem) error { func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer) onePasswordItem.Finalizers = utils.RemoveString(onePasswordItem.Finalizers, finalizer)
return r.Update(context.Background(), opSecret) return r.Update(ctx, onePasswordItem)
} }
func (r *OnePasswordItemReconciler) handleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, req ctrl.Request) error { func (r *OnePasswordItemReconciler) handleOnePasswordItem(ctx context.Context, resource *onepasswordv1.OnePasswordItem, _ ctrl.Request) error {
secretName := resource.GetName() secretName := resource.GetName()
labels := resource.Labels labels := resource.Labels
secretType := resource.Type secretType := resource.Type
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation] autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, resource.Spec.ItemPath) item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, resource.Spec.ItemPath)
if err != nil { if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err) return fmt.Errorf("failed to retrieve item: %w", err)
} }
// Create owner reference. // Create owner reference.
gvk, err := apiutil.GVKForObject(resource, r.Scheme) gvk, err := apiutil.GVKForObject(resource, r.Scheme)
if err != nil { if err != nil {
return fmt.Errorf("could not to retrieve group version kind: %v", err) return fmt.Errorf("could not to retrieve group version kind: %w", err)
} }
ownerRef := &metav1.OwnerReference{ ownerRef := &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(), APIVersion: gvk.GroupVersion().String(),
@@ -181,10 +181,10 @@ func (r *OnePasswordItemReconciler) handleOnePasswordItem(resource *onepasswordv
UID: resource.GetUID(), UID: resource.GetUID(),
} }
return kubeSecrets.CreateKubernetesSecretFromItem(r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef) return kubeSecrets.CreateKubernetesSecretFromItem(ctx, r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef)
} }
func (r *OnePasswordItemReconciler) updateStatus(resource *onepasswordv1.OnePasswordItem, err error) error { func (r *OnePasswordItemReconciler) updateStatus(ctx context.Context, resource *onepasswordv1.OnePasswordItem, err error) error {
existingCondition := findCondition(resource.Status.Conditions, onepasswordv1.OnePasswordItemReady) existingCondition := findCondition(resource.Status.Conditions, onepasswordv1.OnePasswordItemReady)
updatedCondition := existingCondition updatedCondition := existingCondition
if err != nil { if err != nil {
@@ -200,7 +200,7 @@ func (r *OnePasswordItemReconciler) updateStatus(resource *onepasswordv1.OnePass
} }
resource.Status.Conditions = []onepasswordv1.OnePasswordItemCondition{updatedCondition} resource.Status.Conditions = []onepasswordv1.OnePasswordItemCondition{updatedCondition}
return r.Status().Update(context.Background(), resource) return r.Status().Update(ctx, resource)
} }
func findCondition(conditions []onepasswordv1.OnePasswordItemCondition, t onepasswordv1.OnePasswordItemConditionType) onepasswordv1.OnePasswordItemCondition { func findCondition(conditions []onepasswordv1.OnePasswordItemCondition, t onepasswordv1.OnePasswordItemConditionType) onepasswordv1.OnePasswordItemCondition {

View File

@@ -2,9 +2,7 @@ package controller
import ( import (
"context" "context"
"fmt"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/mocks"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
@@ -16,6 +14,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/reconcile"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
) )
const ( const (
@@ -32,17 +31,8 @@ var _ = Describe("OnePasswordItem controller", func() {
err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace)) err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { item := item1.ToModel()
item := onepassword.Item{} mockGetItemByIDFunc.Return(item, nil)
item.Fields = []*onepassword.ItemField{}
for k, v := range item1.Data {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = item1.Version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
}) })
Context("Happy path", func() { Context("Happy path", func() {
@@ -71,20 +61,14 @@ var _ = Describe("OnePasswordItem controller", func() {
created := &onepasswordv1.OnePasswordItem{} created := &onepasswordv1.OnePasswordItem{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, key, created) err := k8sClient.Get(ctx, key, created)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
By("Creating the K8s secret successfully") By("Creating the K8s secret successfully")
createdSecret := &v1.Secret{} createdSecret := &v1.Secret{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, key, createdSecret) err := k8sClient.Get(ctx, key, createdSecret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(createdSecret.Data).Should(Equal(item1.SecretData)) Expect(createdSecret.Data).Should(Equal(item1.SecretData))
@@ -99,27 +83,20 @@ var _ = Describe("OnePasswordItem controller", func() {
"password": []byte("##newPassword##"), "password": []byte("##newPassword##"),
"extraField": []byte("dev"), "extraField": []byte("dev"),
} }
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{} item := item2.ToModel()
item.Fields = []*onepassword.ItemField{}
for k, v := range newData { for k, v := range newData {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
}
item.Version = item1.Version + 1
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
} }
mockGetItemByIDFunc.Return(item, nil)
_, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) _, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
updatedSecret := &v1.Secret{} updatedSecret := &v1.Secret{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, key, updatedSecret) err := k8sClient.Get(ctx, key, updatedSecret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(updatedSecret.Data).Should(Equal(newDataByte)) Expect(updatedSecret.Data).Should(Equal(newDataByte))
@@ -178,18 +155,11 @@ var _ = Describe("OnePasswordItem controller", func() {
"ice-cream-type": []byte(iceCream), "ice-cream-type": []byte(iceCream),
} }
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { item := item2.ToModel()
item := onepassword.Item{}
item.Title = "!my sECReT it3m%"
item.Fields = []*onepassword.ItemField{}
for k, v := range testData { for k, v := range testData {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
}
item.Version = item1.Version + 1
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
} }
mockGetItemByIDFunc.Return(item, nil)
By("Creating a new OnePasswordItem successfully") By("Creating a new OnePasswordItem successfully")
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed()) Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
@@ -197,20 +167,14 @@ var _ = Describe("OnePasswordItem controller", func() {
created := &onepasswordv1.OnePasswordItem{} created := &onepasswordv1.OnePasswordItem{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, key, created) err := k8sClient.Get(ctx, key, created)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
By("Creating the K8s secret successfully") By("Creating the K8s secret successfully")
createdSecret := &v1.Secret{} createdSecret := &v1.Secret{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, key, createdSecret) err := k8sClient.Get(ctx, key, createdSecret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(createdSecret.Data).Should(Equal(expectedData)) Expect(createdSecret.Data).Should(Equal(expectedData))
@@ -319,13 +283,55 @@ var _ = Describe("OnePasswordItem controller", func() {
secret := &v1.Secret{} secret := &v1.Secret{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, key, secret) err := k8sClient.Get(ctx, key, secret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(secret.Type).Should(Equal(v1.SecretType(customType))) 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() { Context("Unhappy path", func() {
@@ -355,20 +361,14 @@ var _ = Describe("OnePasswordItem controller", func() {
secret := &v1.Secret{} secret := &v1.Secret{}
Eventually(func() bool { Eventually(func() bool {
err := k8sClient.Get(ctx, key, secret) err := k8sClient.Get(ctx, key, secret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
By("Failing to update K8s secret") By("Failing to update K8s secret")
Eventually(func() bool { Eventually(func() bool {
secret.Type = v1.SecretTypeBasicAuth secret.Type = v1.SecretTypeBasicAuth
err := k8sClient.Update(ctx, secret) err := k8sClient.Update(ctx, secret)
if err != nil { return err == nil
return false
}
return true
}, timeout, interval).Should(BeFalse()) }, timeout, interval).Should(BeFalse())
}) })

View File

@@ -26,15 +26,15 @@ package controller
import ( import (
"context" "context"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"testing" "testing"
"time" "time"
"github.com/1Password/onepassword-operator/pkg/mocks"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@@ -45,6 +45,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/log/zap"
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1" 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 // +kubebuilder:scaffold:imports
) )
@@ -78,8 +80,11 @@ var (
cancel context.CancelFunc cancel context.CancelFunc
onePasswordItemReconciler *OnePasswordItemReconciler onePasswordItemReconciler *OnePasswordItemReconciler
deploymentReconciler *DeploymentReconciler deploymentReconciler *DeploymentReconciler
mockGetItemByIDFunc *mock.Call
item1 = &TestItem{ item1 = &TestItem{
ItemID: "nwrhuano7bcwddcviubpp4mhfq",
VaultID: "hfnjvi6aymbsnfc2xeeoheizda",
Name: "test-item", Name: "test-item",
Version: 123, Version: 123,
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizda/items/nwrhuano7bcwddcviubpp4mhfq", Path: "vaults/hfnjvi6aymbsnfc2xeeoheizda/items/nwrhuano7bcwddcviubpp4mhfq",
@@ -94,6 +99,8 @@ var (
} }
item2 = &TestItem{ item2 = &TestItem{
ItemID: "nwrhuano7bcwddcviubpp4mhf2",
VaultID: "hfnjvi6aymbsnfc2xeeoheizd2",
Name: "test-item2", Name: "test-item2",
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizd2/items/nwrhuano7bcwddcviubpp4mhf2", Path: "vaults/hfnjvi6aymbsnfc2xeeoheizd2/items/nwrhuano7bcwddcviubpp4mhf2",
Version: 456, Version: 456,
@@ -109,6 +116,8 @@ var (
) )
type TestItem struct { type TestItem struct {
ItemID string
VaultID string
Name string Name string
Version int Version int
Path string Path string
@@ -116,6 +125,20 @@ type TestItem struct {
SecretData map[string][]byte 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) { func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail) RegisterFailHandler(Fail)
@@ -133,6 +156,11 @@ var _ = BeforeSuite(func() {
ErrorIfCRDPathMissing: true, ErrorIfCRDPathMissing: true,
} }
// Retrieve the first found binary directory to allow running tests from IDEs
if getFirstFoundEnvTestBinaryDir() != "" {
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
}
var err error var err error
// cfg is defined in this file globally. // cfg is defined in this file globally.
cfg, err = testEnv.Start() cfg, err = testEnv.Start()
@@ -153,12 +181,13 @@ var _ = BeforeSuite(func() {
}) })
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
opConnectClient := &mocks.TestClient{} mockOpClient := &mocks.TestClient{}
mockGetItemByIDFunc = mockOpClient.On("GetItemByID", mock.Anything, mock.Anything)
onePasswordItemReconciler = &OnePasswordItemReconciler{ onePasswordItemReconciler = &OnePasswordItemReconciler{
Client: k8sManager.GetClient(), Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(), Scheme: k8sManager.GetScheme(),
OpConnectClient: opConnectClient, OpClient: mockOpClient,
} }
err = (onePasswordItemReconciler).SetupWithManager(k8sManager) err = (onePasswordItemReconciler).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
@@ -167,7 +196,7 @@ var _ = BeforeSuite(func() {
deploymentReconciler = &DeploymentReconciler{ deploymentReconciler = &DeploymentReconciler{
Client: k8sManager.GetClient(), Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(), Scheme: k8sManager.GetScheme(),
OpConnectClient: opConnectClient, OpClient: mockOpClient,
OpAnnotationRegExp: r, OpAnnotationRegExp: r,
} }
err = (deploymentReconciler).SetupWithManager(k8sManager) err = (deploymentReconciler).SetupWithManager(k8sManager)
@@ -187,3 +216,26 @@ var _ = AfterSuite(func() {
err := testEnv.Stop() err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred()) 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

@@ -2,20 +2,16 @@ package kubernetessecrets
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"reflect"
"regexp" "regexp"
"strings" "strings"
"reflect" "github.com/1Password/onepassword-operator/pkg/onepassword/model"
errs "errors"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/utils" "github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation" kubeValidate "k8s.io/apimachinery/pkg/util/validation"
@@ -30,33 +26,45 @@ const VersionAnnotation = OnepasswordPrefix + "/item-version"
const ItemPathAnnotation = OnepasswordPrefix + "/item-path" const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable") var ErrCannotUpdateSecretType = errors.New("cannot change secret type: secret type is immutable")
var log = logf.Log var log = logf.Log
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) 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) itemVersion := fmt.Sprint(item.Version)
secretAnnotations := map[string]string{ secretAnnotations := map[string]string{
VersionAnnotation: itemVersion, VersionAnnotation: itemVersion,
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID), ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID),
} }
if autoRestart != "" { if autoRestart != "" {
_, err := utils.StringToBool(autoRestart) _, err := utils.StringToBool(autoRestart)
if err != nil { if err != nil {
return fmt.Errorf("Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) return fmt.Errorf("error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false",
RestartDeploymentsAnnotation, secretName,
)
} }
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
} }
// "Opaque" and "" secret types are treated the same by Kubernetes. // "Opaque" and "" secret types are treated the same by Kubernetes.
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item, ownerRef) secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels,
secretType, *item, ownerRef)
currentSecret := &corev1.Secret{} currentSecret := &corev1.Secret{}
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) err := kubeClient.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
if err != nil && errors.IsNotFound(err) { if err != nil && apierrors.IsNotFound(err) {
log.Info(fmt.Sprintf("Creating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) 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 { } else if err != nil {
return err return err
} }
@@ -79,20 +87,29 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
currentLabels := currentSecret.Labels currentLabels := currentSecret.Labels
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) { if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) {
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
currentSecret.ObjectMeta.Annotations = secretAnnotations currentSecret.Annotations = secretAnnotations
currentSecret.ObjectMeta.Labels = labels currentSecret.Labels = labels
currentSecret.Data = secret.Data currentSecret.Data = secret.Data
if err := kubeClient.Update(context.Background(), currentSecret); err != nil { if err := kubeClient.Update(ctx, currentSecret); err != nil {
return fmt.Errorf("Kubernetes secret update failed: %w", err) return fmt.Errorf("kubernetes secret update failed: %w", err)
} }
return nil return nil
} }
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation])) log.Info(fmt.Sprintf("Secret with name %v and version %v already exists",
secret.Name, secret.Annotations[VersionAnnotation],
))
return nil return nil
} }
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *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 var ownerRefs []metav1.OwnerReference
if ownerRef != nil { if ownerRef != nil {
ownerRefs = []metav1.OwnerReference{*ownerRef} ownerRefs = []metav1.OwnerReference{*ownerRef}
@@ -111,7 +128,7 @@ func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotation
} }
} }
func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte { func BuildKubernetesSecretData(fields []model.ItemField, files []model.File) map[string][]byte {
secretData := map[string][]byte{} secretData := map[string][]byte{}
for i := 0; i < len(fields); i++ { for i := 0; i < len(fields); i++ {
if fields[i].Value != "" { if fields[i].Value != "" {
@@ -124,7 +141,7 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepass
for _, file := range files { for _, file := range files {
content, err := file.Content() content, err := file.Content()
if err != nil { if err != nil {
log.Error(err, "Could not load contents of file %s", file.Name) log.Error(err, fmt.Sprintf("Could not load contents of file %s", file.Name))
continue continue
} }
if content != nil { if content != nil {

View File

@@ -6,37 +6,44 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation" kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
) )
const restartDeploymentAnnotation = "false" const (
restartDeploymentAnnotation = "false"
testNamespace = "test"
testItemUUID = "h46bb3jddvay7nxopfhvlwg35q"
testVaultUUID = "hfnjvi6aymbsnfc2xeeoheizda"
)
func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
ctx := context.Background()
secretName := "test-secret-name" secretName := "test-secret-name"
namespace := "test" namespace := testNamespace
item := onepassword.Item{} item := model.Item{}
item.Fields = generateFields(5) item.Fields = generateFields(5)
item.Version = 123 item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" item.VaultID = testVaultUUID
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = testItemUUID
kubeClient := fake.NewClientBuilder().Build() kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{} secretLabels := map[string]string{}
secretType := "" secretType := ""
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) err := CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &item, restartDeploymentAnnotation,
secretLabels, secretType, nil)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
createdSecret := &corev1.Secret{} 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 { if err != nil {
t.Errorf("Secret was not created: %v", err) t.Errorf("Secret was not created: %v", err)
@@ -46,14 +53,15 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
} }
func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) { func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
ctx := context.Background()
secretName := "test-secret-name" secretName := "test-secret-name"
namespace := "test" namespace := testNamespace
item := onepassword.Item{} item := model.Item{}
item.Fields = generateFields(5) item.Fields = generateFields(5)
item.Version = 123 item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" item.VaultID = testVaultUUID
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = testItemUUID
kubeClient := fake.NewClientBuilder().Build() kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{} secretLabels := map[string]string{}
@@ -65,15 +73,19 @@ func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
Name: "test-deployment", Name: "test-deployment",
UID: types.UID("test-uid"), UID: types.UID("test-uid"),
} }
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, ownerRef) err := CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &item, restartDeploymentAnnotation,
secretLabels, secretType, ownerRef)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
createdSecret := &corev1.Secret{} 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("Unexpected error: %v", err)
}
// Check owner references. // Check owner references.
gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences gotOwnerRefs := createdSecret.OwnerReferences
if len(gotOwnerRefs) != 1 { if len(gotOwnerRefs) != 1 {
t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs)) t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs))
} }
@@ -91,37 +103,40 @@ func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
} }
func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
ctx := context.Background()
secretName := "test-secret-update" secretName := "test-secret-update"
namespace := "test" namespace := testNamespace
item := onepassword.Item{} item := model.Item{}
item.Fields = generateFields(5) item.Fields = generateFields(5)
item.Version = 123 item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" item.VaultID = testVaultUUID
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = testItemUUID
kubeClient := fake.NewClientBuilder().Build() kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{} secretLabels := map[string]string{}
secretType := "" secretType := ""
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) err := CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &item, restartDeploymentAnnotation,
secretLabels, secretType, nil)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
// Updating kubernetes secret with new item // Updating kubernetes secret with new item
newItem := onepassword.Item{} newItem := model.Item{}
newItem.Fields = generateFields(6) newItem.Fields = generateFields(6)
newItem.Version = 456 newItem.Version = 456
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" newItem.VaultID = testVaultUUID
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" newItem.ID = testItemUUID
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil) err = CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation,
secretLabels, secretType, nil)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
updatedSecret := &corev1.Secret{} 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 { if err != nil {
t.Errorf("Secret was not found: %v", err) t.Errorf("Secret was not found: %v", err)
@@ -147,7 +162,7 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
annotations := map[string]string{ annotations := map[string]string{
annotationKey: annotationValue, annotationKey: annotationValue,
} }
item := onepassword.Item{} item := model.Item{}
item.Fields = generateFields(5) item.Fields = generateFields(5)
labels := map[string]string{} labels := map[string]string{}
secretType := "" secretType := ""
@@ -173,10 +188,10 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
"annotationKey": "annotationValue", "annotationKey": "annotationValue",
} }
labels := map[string]string{} labels := map[string]string{}
item := onepassword.Item{} item := model.Item{}
secretType := "" secretType := ""
item.Fields = []*onepassword.ItemField{ item.Fields = []model.ItemField{
{ {
Label: "label w%th invalid ch!rs-", Label: "label w%th invalid ch!rs-",
Value: "value1", Value: "value1",
@@ -206,25 +221,27 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
} }
func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) { func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
ctx := context.Background()
secretName := "tls-test-secret-name" secretName := "tls-test-secret-name"
namespace := "test" namespace := testNamespace
item := onepassword.Item{} item := model.Item{}
item.Fields = generateFields(5) item.Fields = generateFields(5)
item.Version = 123 item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" item.VaultID = testVaultUUID
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = testItemUUID
kubeClient := fake.NewClientBuilder().Build() kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{} secretLabels := map[string]string{}
secretType := "kubernetes.io/tls" secretType := "kubernetes.io/tls"
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) err := CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &item, restartDeploymentAnnotation,
secretLabels, secretType, nil)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
createdSecret := &corev1.Secret{} 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 { if err != nil {
t.Errorf("Secret was not created: %v", err) t.Errorf("Secret was not created: %v", err)
@@ -235,13 +252,13 @@ func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
} }
} }
func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { func compareAnnotationsToItem(annotations map[string]string, item model.Item, t *testing.T) {
actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
if err != nil { if err != nil {
t.Errorf("Was unable to parse Item Path") t.Errorf("Was unable to parse Item Path")
} }
if actualVaultId != item.Vault.ID { if actualVaultId != item.VaultID {
t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId) t.Errorf("Expected annotation vault id to be %v but was %v", item.VaultID, actualVaultId)
} }
if actualItemId != item.ID { if actualItemId != item.ID {
t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId) t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId)
@@ -251,11 +268,13 @@ func compareAnnotationsToItem(annotations map[string]string, item onepassword.It
} }
if annotations[RestartDeploymentsAnnotation] != "false" { 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++ { for i := 0; i < len(actualFields); i++ {
value, found := secretData[actualFields[i].Label] value, found := secretData[actualFields[i].Label]
if !found { if !found {
@@ -267,14 +286,13 @@ func compareFields(actualFields []*onepassword.ItemField, secretData map[string]
} }
} }
func generateFields(numToGenerate int) []*onepassword.ItemField { func generateFields(numToGenerate int) []model.ItemField {
fields := []*onepassword.ItemField{} fields := []model.ItemField{}
for i := 0; i < numToGenerate; i++ { for i := 0; i < numToGenerate; i++ {
field := onepassword.ItemField{ fields = append(fields, model.ItemField{
Label: "key" + fmt.Sprint(i), Label: "key" + fmt.Sprint(i),
Value: "value" + fmt.Sprint(i), Value: "value" + fmt.Sprint(i),
} })
fields = append(fields, &field)
} }
return fields return fields
} }
@@ -284,7 +302,10 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
return splitPath[1], splitPath[3], nil return splitPath[1], splitPath[3], nil
} }
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) return "", "", fmt.Errorf(
"%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`",
path,
)
} }
func validLabel(v string) bool { func validLabel(v string) bool {

View File

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

View File

@@ -1,151 +1,39 @@
package mocks package mocks
import ( 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 { type TestClient struct {
GetVaultsFunc func() ([]onepassword.Vault, error) mock.Mock
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetVaultFunc func(uuid string) (*onepassword.Vault, error)
GetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error)
GetVaultByTitleFunc func(title string) (*onepassword.Vault, error)
GetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error)
GetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error)
GetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error)
GetItemsFunc func(vaultQuery string) ([]onepassword.Item, error)
GetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error)
CreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
UpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
DeleteItemFunc func(item *onepassword.Item, vaultQuery string) error
DeleteItemByIDFunc func(itemUUID string, vaultQuery string) error
DeleteItemByTitleFunc func(title string, vaultQuery string) error
GetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error)
GetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error)
GetFileContentFunc func(file *onepassword.File) ([]byte, error)
DownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error)
LoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error
LoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error
LoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error
LoadStructFunc func(config interface{}) error
} }
var ( func (tc *TestClient) GetItemByID(ctx context.Context, vaultID, itemID string) (*model.Item, error) {
DoGetVaultsFunc func() ([]onepassword.Vault, error) args := tc.Called(vaultID, itemID)
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) if args.Get(0) == nil {
DoGetVaultFunc func(uuid string) (*onepassword.Vault, error) return nil, args.Error(1)
DoGetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error) }
DoGetVaultByTitleFunc func(title string) (*onepassword.Vault, error) return args.Get(0).(*model.Item), args.Error(1)
DoGetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error)
DoGetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error)
DoGetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error)
DoGetItemsFunc func(vaultQuery string) ([]onepassword.Item, error)
DoGetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error)
DoCreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
DoUpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
DoDeleteItemFunc func(item *onepassword.Item, vaultQuery string) error
DoDeleteItemByIDFunc func(itemUUID string, vaultQuery string) error
DoDeleteItemByTitleFunc func(title string, vaultQuery string) error
DoGetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error)
DoGetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error)
DoGetFileContentFunc func(file *onepassword.File) ([]byte, error)
DoDownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error)
DoLoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error
DoLoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error
DoLoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error
DoLoadStructFunc func(config interface{}) error
)
// Do is the mock client's `Do` func
func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
return DoGetVaultsFunc()
} }
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { func (tc *TestClient) GetItemsByTitle(ctx context.Context, vaultID, itemTitle string) ([]model.Item, error) {
return DoGetVaultsByTitleFunc(title) args := tc.Called(vaultID, itemTitle)
return args.Get(0).([]model.Item), args.Error(1)
} }
func (m *TestClient) GetVault(vaultQuery string) (*onepassword.Vault, error) { func (tc *TestClient) GetFileContent(ctx context.Context, vaultID, itemID, fileID string) ([]byte, error) {
return DoGetVaultFunc(vaultQuery) 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) GetVaultByUUID(uuid string) (*onepassword.Vault, error) { func (tc *TestClient) GetVaultsByTitle(ctx context.Context, title string) ([]model.Vault, error) {
return DoGetVaultByUUIDFunc(uuid) args := tc.Called(title)
} return args.Get(0).([]model.Vault), args.Error(1)
func (m *TestClient) GetVaultByTitle(title string) (*onepassword.Vault, error) {
return DoGetVaultByTitleFunc(title)
}
func (m *TestClient) GetItem(itemQuery string, vaultQuery string) (*onepassword.Item, error) {
return DoGetItemFunc(itemQuery, vaultQuery)
}
func (m *TestClient) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) {
return DoGetItemByUUIDFunc(uuid, vaultQuery)
}
func (m *TestClient) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) {
return DoGetItemByTitleFunc(title, vaultQuery)
}
func (m *TestClient) GetItems(vaultQuery string) ([]onepassword.Item, error) {
return DoGetItemsFunc(vaultQuery)
}
func (m *TestClient) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) {
return DoGetItemsByTitleFunc(title, vaultQuery)
}
func (m *TestClient) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
return DoCreateItemFunc(item, vaultQuery)
}
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
return DoUpdateItemFunc(item, vaultQuery)
}
func (m *TestClient) DeleteItem(item *onepassword.Item, vaultQuery string) error {
return DoDeleteItemFunc(item, vaultQuery)
}
func (m *TestClient) DeleteItemByID(itemUUID string, vaultQuery string) error {
return DoDeleteItemByIDFunc(itemUUID, vaultQuery)
}
func (m *TestClient) DeleteItemByTitle(title string, vaultQuery string) error {
return DoDeleteItemByTitleFunc(title, vaultQuery)
}
func (m *TestClient) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) {
return DoGetFilesFunc(itemQuery, vaultQuery)
}
func (m *TestClient) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) {
return DoGetFileFunc(uuid, itemQuery, vaultQuery)
}
func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) {
return DoGetFileContentFunc(file)
}
func (m *TestClient) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) {
return DoDownloadFileFunc(file, targetDirectory, overwrite)
}
func (m *TestClient) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error {
return DoLoadStructFromItemByUUIDFunc(config, itemUUID, vaultQuery)
}
func (m *TestClient) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error {
return DoLoadStructFromItemByTitleFunc(config, itemTitle, vaultQuery)
}
func (m *TestClient) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error {
return DoLoadStructFromItemFunc(config, itemQuery, vaultQuery)
}
func (m *TestClient) LoadStruct(config interface{}) error {
return DoLoadStructFunc(config)
} }

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 { func AreAnnotationsUsingSecrets(annotations map[string]string, secrets map[string]*corev1.Secret) bool {
_, ok := secrets[annotations[NameAnnotation]] _, ok := secrets[annotations[NameAnnotation]]
if ok { return ok
return true
}
return false
} }
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]] secret, ok := secrets[annotations[NameAnnotation]]
if ok { if ok {
updatedDeploymentSecrets[secret.Name] = secret updatedDeploymentSecrets[secret.Name] = secret

View File

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

View File

@@ -0,0 +1,110 @@
package testing
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
sdk "github.com/1password/onepassword-sdk-go"
)
func CreateConnectItem() *onepassword.Item {
return &onepassword.Item{
ID: "test-id",
Vault: onepassword.ItemVault{ID: "test-vault-id"},
Version: 1,
Tags: []string{"tag1", "tag2"},
Fields: []*onepassword.ItemField{
{Label: "label1", Value: "value1"},
{Label: "label2", Value: "value2"},
},
Files: []*onepassword.File{
{ID: "file-id-1", Name: "file1.txt", Size: 1234},
{ID: "file-id-2", Name: "file2.txt", Size: 1234},
},
}
}
func CreateSDKItem() *sdk.Item {
return &sdk.Item{
ID: "test-id",
VaultID: "test-vault-id",
Version: 1,
Tags: []string{"tag1", "tag2"},
Fields: []sdk.ItemField{
{Title: "label1", Value: "value1"},
{Title: "label2", Value: "value2"},
},
Files: []sdk.ItemFile{
{Attributes: sdk.FileAttributes{ID: "file-id-1", Name: "file1.txt", Size: 1234}},
{Attributes: sdk.FileAttributes{ID: "file-id-2", Name: "file2.txt", Size: 1234}},
},
CreatedAt: time.Now(),
}
}
func CreateSDKItemOverview() *sdk.ItemOverview {
return &sdk.ItemOverview{
ID: "test-id",
Title: "item-title",
VaultID: "test-vault-id",
Tags: []string{"tag1", "tag2"},
CreatedAt: time.Now(),
}
}
func CheckConnectItemMapping(t *testing.T, expected *onepassword.Item, actual *model.Item) {
t.Helper()
require.Equal(t, expected.ID, actual.ID)
require.Equal(t, expected.Vault.ID, actual.VaultID)
require.Equal(t, expected.Version, actual.Version)
require.ElementsMatch(t, expected.Tags, actual.Tags)
for i, field := range expected.Fields {
require.Equal(t, field.Label, actual.Fields[i].Label)
require.Equal(t, field.Value, actual.Fields[i].Value)
}
for i, file := range expected.Files {
require.Equal(t, file.ID, actual.Files[i].ID)
require.Equal(t, file.Name, actual.Files[i].Name)
require.Equal(t, file.Size, actual.Files[i].Size)
}
require.Equal(t, expected.CreatedAt, actual.CreatedAt)
}
func CheckSDKItemMapping(t *testing.T, expected *sdk.Item, actual *model.Item) {
t.Helper()
require.Equal(t, expected.ID, actual.ID)
require.Equal(t, expected.VaultID, actual.VaultID)
require.Equal(t, int(expected.Version), actual.Version)
require.ElementsMatch(t, expected.Tags, actual.Tags)
for i, field := range expected.Fields {
require.Equal(t, field.Title, actual.Fields[i].Label)
require.Equal(t, field.Value, actual.Fields[i].Value)
}
for i, file := range expected.Files {
require.Equal(t, file.Attributes.ID, actual.Files[i].ID)
require.Equal(t, file.Attributes.Name, actual.Files[i].Name)
require.Equal(t, int(file.Attributes.Size), actual.Files[i].Size)
}
require.Equal(t, expected.CreatedAt, actual.CreatedAt)
}
func CheckSDKItemOverviewMapping(t *testing.T, expected *sdk.ItemOverview, actual *model.Item) {
t.Helper()
require.Equal(t, expected.ID, actual.ID)
require.Equal(t, expected.VaultID, actual.VaultID)
require.ElementsMatch(t, expected.Tags, actual.Tags)
require.Equal(t, expected.CreatedAt, actual.CreatedAt)
}

View File

@@ -0,0 +1,134 @@
package mock
import (
"github.com/stretchr/testify/mock"
"github.com/1Password/connect-sdk-go/onepassword"
)
// ConnectClientMock is a mock implementation of the ConnectClient interface
type ConnectClientMock struct {
mock.Mock
}
func (c *ConnectClientMock) GetVaults() ([]onepassword.Vault, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetVault(uuid string) (*onepassword.Vault, error) {
args := c.Called(uuid)
return args.Get(0).(*onepassword.Vault), args.Error(1)
}
func (c *ConnectClientMock) GetVaultByUUID(uuid string) (*onepassword.Vault, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetVaultByTitle(title string) (*onepassword.Vault, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
args := c.Called(title)
return args.Get(0).([]onepassword.Vault), args.Error(1)
}
func (c *ConnectClientMock) GetItems(vaultQuery string) ([]onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) {
args := c.Called(uuid, vaultQuery)
return args.Get(0).(*onepassword.Item), args.Error(1)
}
func (c *ConnectClientMock) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) {
args := c.Called(title, vaultQuery)
return args.Get(0).([]onepassword.Item), args.Error(1)
}
func (c *ConnectClientMock) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) DeleteItem(item *onepassword.Item, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) DeleteItemByID(itemUUID string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) DeleteItemByTitle(title string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetFileContent(file *onepassword.File) ([]byte, error) {
args := c.Called(file)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}
func (c *ConnectClientMock) DownloadFile(
file *onepassword.File,
targetDirectory string,
overwrite bool,
) (string, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) LoadStruct(config interface{}) error {
// Only implement this if mocking is needed
panic("implement me")
}

View File

@@ -0,0 +1,97 @@
package mock
import (
"context"
"github.com/stretchr/testify/mock"
sdk "github.com/1password/onepassword-sdk-go"
)
type VaultAPIMock struct {
mock.Mock
}
func (v *VaultAPIMock) List(ctx context.Context) ([]sdk.VaultOverview, error) {
args := v.Called(ctx)
return args.Get(0).([]sdk.VaultOverview), args.Error(1)
}
type ItemAPIMock struct {
mock.Mock
FilesAPI sdk.ItemsFilesAPI
}
func (i *ItemAPIMock) Create(ctx context.Context, params sdk.ItemCreateParams) (sdk.Item, error) {
// TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) Get(ctx context.Context, vaultID string, itemID string) (sdk.Item, error) {
args := i.Called(ctx, vaultID, itemID)
return args.Get(0).(sdk.Item), args.Error(1)
}
func (i *ItemAPIMock) Put(ctx context.Context, item sdk.Item) (sdk.Item, error) {
// TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) Delete(ctx context.Context, vaultID string, itemID string) error {
// TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) Archive(ctx context.Context, vaultID string, itemID string) error {
// TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) List(
ctx context.Context,
vaultID string,
filters ...sdk.ItemListFilter,
) ([]sdk.ItemOverview, error) {
args := i.Called(ctx, vaultID, filters)
return args.Get(0).([]sdk.ItemOverview), args.Error(1)
}
func (i *ItemAPIMock) Shares() sdk.ItemsSharesAPI {
// TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) Files() sdk.ItemsFilesAPI {
return i.FilesAPI
}
type FileAPIMock struct {
mock.Mock
}
func (f *FileAPIMock) Attach(ctx context.Context, item sdk.Item, fileParams sdk.FileCreateParams) (sdk.Item, error) {
// TODO implement me
panic("implement me")
}
func (f *FileAPIMock) Delete(ctx context.Context, item sdk.Item, sectionID string, fieldID string) (sdk.Item, error) {
// TODO implement me
panic("implement me")
}
func (f *FileAPIMock) ReplaceDocument(
ctx context.Context,
item sdk.Item,
docParams sdk.DocumentCreateParams,
) (sdk.Item, error) {
// TODO implement me
panic("implement me")
}
func (f *FileAPIMock) Read(ctx context.Context, vaultID, itemID string, attributes sdk.FileAttributes) ([]byte, error) {
args := f.Called(ctx, vaultID, itemID, attributes)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}

View File

@@ -18,13 +18,13 @@ var logConnectSetup = logf.Log.WithName("ConnectSetup")
var deploymentPath = "../config/connect/deployment.yaml" var deploymentPath = "../config/connect/deployment.yaml"
var servicePath = "../config/connect/service.yaml" var servicePath = "../config/connect/service.yaml"
func SetupConnect(kubeClient client.Client, deploymentNamespace string) error { func SetupConnect(ctx context.Context, kubeClient client.Client, deploymentNamespace string) error {
err := setupService(kubeClient, servicePath, deploymentNamespace) err := setupService(ctx, kubeClient, servicePath, deploymentNamespace)
if err != nil { if err != nil {
return err return err
} }
err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace) err = setupDeployment(ctx, kubeClient, deploymentPath, deploymentNamespace)
if err != nil { if err != nil {
return err return err
} }
@@ -32,27 +32,40 @@ func SetupConnect(kubeClient client.Client, deploymentNamespace string) error {
return nil return nil
} }
func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { func setupDeployment(
ctx context.Context,
kubeClient client.Client,
deploymentPath string,
deploymentNamespace string,
) error {
existingDeployment := &appsv1.Deployment{} existingDeployment := &appsv1.Deployment{}
// check if deployment has already been created // check if deployment has already been created
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment) err := kubeClient.Get(ctx, types.NamespacedName{
Name: "onepassword-connect",
Namespace: deploymentNamespace,
}, existingDeployment)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") logConnectSetup.Info("No existing Connect deployment found. Creating Deployment")
return createDeployment(kubeClient, deploymentPath, deploymentNamespace) return createDeployment(ctx, kubeClient, deploymentPath, deploymentNamespace)
} }
} }
return err return err
} }
func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { func createDeployment(
ctx context.Context,
kubeClient client.Client,
deploymentPath string,
deploymentNamespace string,
) error {
deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace) deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace)
if err != nil { if err != nil {
return err return err
} }
err = kubeClient.Create(context.Background(), deployment) err = kubeClient.Create(ctx, deployment)
if err != nil { if err != nil {
return err return err
} }
@@ -78,21 +91,29 @@ func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*
return deployment, nil return deployment, nil
} }
func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { func setupService(ctx context.Context, kubeClient client.Client, servicePath string, deploymentNamespace string) error {
existingService := &corev1.Service{} existingService := &corev1.Service{}
// check if service has already been created // check if service has already been created
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService) err := kubeClient.Get(ctx, types.NamespacedName{
Name: "onepassword-connect",
Namespace: deploymentNamespace,
}, existingService)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
logConnectSetup.Info("No existing Connect service found. Creating Service") logConnectSetup.Info("No existing Connect service found. Creating Service")
return createService(kubeClient, servicePath, deploymentNamespace) return createService(ctx, kubeClient, servicePath, deploymentNamespace)
} }
} }
return err return err
} }
func createService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { func createService(
ctx context.Context,
kubeClient client.Client,
servicePath string,
deploymentNamespace string,
) error {
f, err := os.Open(servicePath) f, err := os.Open(servicePath)
if err != nil { if err != nil {
return err return err
@@ -108,7 +129,7 @@ func createService(kubeClient client.Client, servicePath string, deploymentNames
return err return err
} }
err = kubeClient.Create(context.Background(), service) err = kubeClient.Create(ctx, service)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -15,6 +15,7 @@ import (
var defaultNamespacedName = types.NamespacedName{Name: "onepassword-connect", Namespace: "default"} var defaultNamespacedName = types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}
func TestServiceSetup(t *testing.T) { func TestServiceSetup(t *testing.T) {
ctx := context.Background()
// Register operator types with the runtime scheme. // Register operator types with the runtime scheme.
s := scheme.Scheme s := scheme.Scheme
@@ -25,7 +26,7 @@ func TestServiceSetup(t *testing.T) {
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
err := setupService(client, "../../config/connect/service.yaml", defaultNamespacedName.Namespace) err := setupService(ctx, client, "../../config/connect/service.yaml", defaultNamespacedName.Namespace)
if err != nil { if err != nil {
t.Errorf("Error Setting Up Connect: %v", err) t.Errorf("Error Setting Up Connect: %v", err)
@@ -33,13 +34,14 @@ func TestServiceSetup(t *testing.T) {
// check that service was created // check that service was created
service := &corev1.Service{} service := &corev1.Service{}
err = client.Get(context.TODO(), defaultNamespacedName, service) err = client.Get(ctx, defaultNamespacedName, service)
if err != nil { if err != nil {
t.Errorf("Error Setting Up Connect service: %v", err) t.Errorf("Error Setting Up Connect service: %v", err)
} }
} }
func TestDeploymentSetup(t *testing.T) { func TestDeploymentSetup(t *testing.T) {
ctx := context.Background()
// Register operator types with the runtime scheme. // Register operator types with the runtime scheme.
s := scheme.Scheme s := scheme.Scheme
@@ -50,7 +52,7 @@ func TestDeploymentSetup(t *testing.T) {
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
err := setupDeployment(client, "../../config/connect/deployment.yaml", defaultNamespacedName.Namespace) err := setupDeployment(ctx, client, "../../config/connect/deployment.yaml", defaultNamespacedName.Namespace)
if err != nil { if err != nil {
t.Errorf("Error Setting Up Connect: %v", err) t.Errorf("Error Setting Up Connect: %v", err)
@@ -58,7 +60,7 @@ func TestDeploymentSetup(t *testing.T) {
// check that deployment was created // check that deployment was created
deployment := &appsv1.Deployment{} deployment := &appsv1.Deployment{}
err = client.Get(context.TODO(), defaultNamespacedName, deployment) err = client.Get(ctx, defaultNamespacedName, deployment)
if err != nil { if err != nil {
t.Errorf("Error Setting Up Connect deployment: %v", err) t.Errorf("Error Setting Up Connect deployment: %v", err)
} }

View File

@@ -28,7 +28,11 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string
return false return false
} }
func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { func AppendUpdatedContainerSecrets(
containers []corev1.Container,
secrets map[string]*corev1.Secret,
updatedDeploymentSecrets map[string]*corev1.Secret,
) map[string]*corev1.Secret {
for i := 0; i < len(containers); i++ { for i := 0; i < len(containers); i++ {
envVariables := containers[i].Env envVariables := containers[i].Env
for j := 0; j < len(envVariables); j++ { for j := 0; j < len(envVariables); j++ {
@@ -42,7 +46,7 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st
envFromVariables := containers[i].EnvFrom envFromVariables := containers[i].EnvFrom
for j := 0; j < len(envFromVariables); j++ { for j := 0; j < len(envFromVariables); j++ {
if envFromVariables[j].SecretRef != nil { if envFromVariables[j].SecretRef != nil {
secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name] secret, ok := secrets[envFromVariables[j].SecretRef.Name]
if ok { if ok {
updatedDeploymentSecrets[secret.Name] = secret updatedDeploymentSecrets[secret.Name] = secret
} }

View File

@@ -9,10 +9,15 @@ func IsDeploymentUsingSecrets(deployment *appsv1.Deployment, secrets map[string]
volumes := deployment.Spec.Template.Spec.Volumes volumes := deployment.Spec.Template.Spec.Volumes
containers := deployment.Spec.Template.Spec.Containers containers := deployment.Spec.Template.Spec.Containers
containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) containers = append(containers, deployment.Spec.Template.Spec.InitContainers...)
return AreAnnotationsUsingSecrets(deployment.Annotations, secrets) || AreContainersUsingSecrets(containers, secrets) || AreVolumesUsingSecrets(volumes, secrets) return AreAnnotationsUsingSecrets(deployment.Annotations, secrets) ||
AreContainersUsingSecrets(containers, secrets) ||
AreVolumesUsingSecrets(volumes, secrets)
} }
func GetUpdatedSecretsForDeployment(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) map[string]*corev1.Secret { func GetUpdatedSecretsForDeployment(
deployment *appsv1.Deployment,
secrets map[string]*corev1.Secret,
) map[string]*corev1.Secret {
volumes := deployment.Spec.Template.Spec.Volumes volumes := deployment.Spec.Template.Spec.Volumes
containers := deployment.Spec.Template.Spec.Containers containers := deployment.Spec.Template.Spec.Containers
containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) containers = append(containers, deployment.Spec.Template.Spec.InitContainers...)

View File

@@ -1,42 +1,44 @@
package onepassword package onepassword
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
) )
var logger = logf.Log.WithName("retrieve_item") var logger = logf.Log.WithName("retrieve_item")
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) { func GetOnePasswordItemByPath(ctx context.Context, opClient opclient.Client, path string) (*model.Item, error) {
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path) vaultNameOrID, itemNameOrID, err := ParseVaultAndItemFromPath(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
vaultId, err := getVaultId(opConnectClient, vaultValue) vaultID, err := getVaultID(ctx, opClient, vaultNameOrID)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to 'getVaultID' for vaultNameOrID='%s': %w", vaultNameOrID, err)
} }
itemId, err := getItemId(opConnectClient, itemValue, vaultId) itemID, err := getItemID(ctx, opClient, vaultID, itemNameOrID)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("faild to 'getItemID' for vaultID='%s' and itemNameOrID='%s': %w", vaultID, itemNameOrID, err)
} }
item, err := opConnectClient.GetItem(itemId, vaultId) item, err := opClient.GetItemByID(ctx, vaultID, itemID)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("faield to 'GetItemByID' for vaultID='%s' and itemID='%s': %w", vaultID, itemID, err)
} }
for _, file := range item.Files { for i, file := range item.Files {
_, err := opConnectClient.GetFileContent(file) content, err := opClient.GetFileContent(ctx, vaultID, itemID, file.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
item.Files[i].SetContent(content)
} }
return item, nil return item, nil
@@ -47,18 +49,21 @@ func ParseVaultAndItemFromPath(path string) (string, string, error) {
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
return splitPath[1], splitPath[3], nil return splitPath[1], splitPath[3], nil
} }
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) return "", "", fmt.Errorf(
"%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`",
path,
)
} }
func getVaultId(client connect.Client, vaultIdentifier string) (string, error) { func getVaultID(ctx context.Context, client opclient.Client, vaultNameOrID string) (string, error) {
if !IsValidClientUUID(vaultIdentifier) { if !IsValidClientUUID(vaultNameOrID) {
vaults, err := client.GetVaultsByTitle(vaultIdentifier) vaults, err := client.GetVaultsByTitle(ctx, vaultNameOrID)
if err != nil { if err != nil {
return "", err return "", err
} }
if len(vaults) == 0 { if len(vaults) == 0 {
return "", fmt.Errorf("No vaults found with identifier %q", vaultIdentifier) return "", fmt.Errorf("no vaults found with identifier %q", vaultNameOrID)
} }
oldestVault := vaults[0] oldestVault := vaults[0]
@@ -68,22 +73,24 @@ func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {
oldestVault = returnedVault oldestVault = returnedVault
} }
} }
logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultIdentifier, oldestVault.ID)) logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.",
len(vaults), vaultNameOrID, oldestVault.ID,
))
} }
vaultIdentifier = oldestVault.ID vaultNameOrID = oldestVault.ID
} }
return vaultIdentifier, nil return vaultNameOrID, nil
} }
func getItemId(client connect.Client, itemIdentifier string, vaultId string) (string, error) { func getItemID(ctx context.Context, client opclient.Client, vaultId, itemNameOrID string) (string, error) {
if !IsValidClientUUID(itemIdentifier) { if !IsValidClientUUID(itemNameOrID) {
items, err := client.GetItemsByTitle(itemIdentifier, vaultId) items, err := client.GetItemsByTitle(ctx, vaultId, itemNameOrID)
if err != nil { if err != nil {
return "", err return "", err
} }
if len(items) == 0 { if len(items) == 0 {
return "", fmt.Errorf("No items found with identifier %q", itemIdentifier) return "", fmt.Errorf("no items found with identifier %q", itemNameOrID)
} }
oldestItem := items[0] oldestItem := items[0]
@@ -93,9 +100,11 @@ func getItemId(client connect.Client, itemIdentifier string, vaultId string) (st
oldestItem = returnedItem oldestItem = returnedItem
} }
} }
logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemIdentifier, oldestItem.ID)) logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.",
len(items), itemNameOrID, oldestItem.ID,
))
} }
itemIdentifier = oldestItem.ID itemNameOrID = oldestItem.ID
} }
return itemIdentifier, nil return itemNameOrID, nil
} }

View File

@@ -0,0 +1,27 @@
package model
import (
"errors"
)
// File represents a file stored in 1Password.
type File struct {
ID string
Name string
Size int
ContentPath string
content []byte
}
// Content returns the content of the file if they have been loaded and returns an error if they have not been loaded.
// Use `client.GetFileContent(file *File)` instead to make sure the content is fetched automatically if not present.
func (f *File) Content() ([]byte, error) {
if f.content == nil {
return nil, errors.New("file content not loaded")
}
return f.content, nil
}
func (f *File) SetContent(content []byte) {
f.content = content
}

View File

@@ -0,0 +1,92 @@
package model
import (
"time"
connect "github.com/1Password/connect-sdk-go/onepassword"
sdk "github.com/1password/onepassword-sdk-go"
)
// Item represents 1Password item.
type Item struct {
ID string
VaultID string
Version int
Tags []string
Fields []ItemField
Files []File
CreatedAt time.Time
}
// FromConnectItem populates the Item from a Connect item.
func (i *Item) FromConnectItem(item *connect.Item) {
i.ID = item.ID
i.VaultID = item.Vault.ID
i.Version = item.Version
i.Tags = append(i.Tags, item.Tags...)
for _, field := range item.Fields {
i.Fields = append(i.Fields, ItemField{
Label: field.Label,
Value: field.Value,
})
}
for _, file := range item.Files {
i.Files = append(i.Files, File{
ID: file.ID,
Name: file.Name,
Size: file.Size,
})
}
i.CreatedAt = item.CreatedAt
}
// FromSDKItem populates the Item from an SDK item.
func (i *Item) FromSDKItem(item *sdk.Item) {
i.ID = item.ID
i.VaultID = item.VaultID
i.Version = int(item.Version)
i.Tags = make([]string, len(item.Tags))
copy(i.Tags, item.Tags)
for _, field := range item.Fields {
i.Fields = append(i.Fields, ItemField{
Label: field.Title,
Value: field.Value,
})
}
for _, file := range item.Files {
i.Files = append(i.Files, File{
ID: file.Attributes.ID,
Name: file.Attributes.Name,
Size: int(file.Attributes.Size),
})
}
// Items of 'Document' category keeps file information in the Document field.
if item.Category == sdk.ItemCategoryDocument {
i.Files = append(i.Files, File{
ID: item.Document.ID,
Name: item.Document.Name,
Size: int(item.Document.Size),
})
}
i.CreatedAt = item.CreatedAt
}
// FromSDKItemOverview populates the Item from an SDK item overview.
func (i *Item) FromSDKItemOverview(item *sdk.ItemOverview) {
i.ID = item.ID
i.VaultID = item.VaultID
i.Tags = make([]string, len(item.Tags))
copy(i.Tags, item.Tags)
i.CreatedAt = item.CreatedAt
}

View File

@@ -0,0 +1,7 @@
package model
// ItemField Representation of a single field on an Item
type ItemField struct {
Label string
Value string
}

View File

@@ -0,0 +1,108 @@
package model
import (
"testing"
"time"
"github.com/stretchr/testify/require"
connect "github.com/1Password/connect-sdk-go/onepassword"
sdk "github.com/1password/onepassword-sdk-go"
)
func TestItem_FromConnectItem(t *testing.T) {
connectItem := &connect.Item{
ID: "test-item-id",
Vault: connect.ItemVault{
ID: "test-vault-id",
},
Version: 1,
Tags: []string{"tag1", "tag2"},
Fields: []*connect.ItemField{
{Label: "field1", Value: "value1"},
{Label: "field2", Value: "value2"},
},
Files: []*connect.File{
{ID: "file1", Name: "file1.txt", Size: 1234},
{ID: "file2", Name: "file2.txt", Size: 1234},
},
CreatedAt: time.Now(),
}
item := &Item{}
item.FromConnectItem(connectItem)
require.Equal(t, connectItem.ID, item.ID)
require.Equal(t, connectItem.Vault.ID, item.VaultID)
require.Equal(t, connectItem.Version, item.Version)
require.ElementsMatch(t, connectItem.Tags, item.Tags)
for i, field := range connectItem.Fields {
require.Equal(t, field.Label, item.Fields[i].Label)
require.Equal(t, field.Value, item.Fields[i].Value)
}
for i, file := range connectItem.Files {
require.Equal(t, file.ID, item.Files[i].ID)
require.Equal(t, file.Name, item.Files[i].Name)
require.Equal(t, file.Size, item.Files[i].Size)
}
require.Equal(t, connectItem.CreatedAt, item.CreatedAt)
}
func TestItem_FromSDKItem(t *testing.T) {
sdkItem := &sdk.Item{
ID: "test-item-id",
VaultID: "test-vault-id",
Version: 1,
Tags: []string{"tag1", "tag2"},
Fields: []sdk.ItemField{
{ID: "1", Title: "field1", Value: "value1"},
{ID: "2", Title: "field2", Value: "value2"},
},
Files: []sdk.ItemFile{
{Attributes: sdk.FileAttributes{Name: "file1.txt", Size: 1234}, FieldID: "file1"},
{Attributes: sdk.FileAttributes{Name: "file2.txt", Size: 1234}, FieldID: "file2"},
},
CreatedAt: time.Now(),
}
item := &Item{}
item.FromSDKItem(sdkItem)
require.Equal(t, sdkItem.ID, item.ID)
require.Equal(t, sdkItem.VaultID, item.VaultID)
require.Equal(t, int(sdkItem.Version), item.Version)
require.ElementsMatch(t, sdkItem.Tags, item.Tags)
for i, field := range sdkItem.Fields {
require.Equal(t, field.Title, item.Fields[i].Label)
require.Equal(t, field.Value, item.Fields[i].Value)
}
for i, file := range sdkItem.Files {
require.Equal(t, file.Attributes.ID, item.Files[i].ID)
require.Equal(t, file.Attributes.Name, item.Files[i].Name)
require.Equal(t, int(file.Attributes.Size), item.Files[i].Size)
}
require.Equal(t, sdkItem.CreatedAt, item.CreatedAt)
}
func TestItem_FromSDKItemOverview(t *testing.T) {
sdkItemOverview := &sdk.ItemOverview{
ID: "test-item-id",
VaultID: "test-vault-id",
Tags: []string{"tag1", "tag2"},
CreatedAt: time.Now(),
}
item := &Item{}
item.FromSDKItemOverview(sdkItemOverview)
require.Equal(t, sdkItemOverview.ID, item.ID)
require.Equal(t, sdkItemOverview.VaultID, item.VaultID)
require.ElementsMatch(t, sdkItemOverview.Tags, item.Tags)
require.Equal(t, sdkItemOverview.CreatedAt, item.CreatedAt)
}

View File

@@ -0,0 +1,23 @@
package model
import (
"time"
connect "github.com/1Password/connect-sdk-go/onepassword"
sdk "github.com/1password/onepassword-sdk-go"
)
type Vault struct {
ID string
CreatedAt time.Time
}
func (v *Vault) FromConnectVault(vault *connect.Vault) {
v.ID = vault.ID
v.CreatedAt = vault.CreatedAt
}
func (v *Vault) FromSDKVault(vault *sdk.VaultOverview) {
v.ID = vault.ID
v.CreatedAt = vault.CreatedAt
}

View File

@@ -0,0 +1,37 @@
package model
import (
"testing"
"time"
"github.com/stretchr/testify/require"
connect "github.com/1Password/connect-sdk-go/onepassword"
sdk "github.com/1password/onepassword-sdk-go"
)
func TestVault_FromConnectVault(t *testing.T) {
connectVault := &connect.Vault{
ID: "test-id",
CreatedAt: time.Now(),
}
vault := &Vault{}
vault.FromConnectVault(connectVault)
require.Equal(t, connectVault.ID, vault.ID)
require.Equal(t, connectVault.CreatedAt, vault.CreatedAt)
}
func TestVault_FromSDKVault(t *testing.T) {
sdkVault := &sdk.VaultOverview{
ID: "test-id",
CreatedAt: time.Now(),
}
vault := &Vault{}
vault.FromSDKVault(sdkVault)
require.Equal(t, sdkVault.ID, vault.ID)
require.Equal(t, sdkVault.CreatedAt, vault.CreatedAt)
}

View File

@@ -8,52 +8,59 @@ import (
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs" "github.com/1Password/onepassword-operator/pkg/logs"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
"github.com/1Password/onepassword-operator/pkg/utils" "github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
) )
const envHostVariable = "OP_HOST" // const envHostVariable = "OP_HOST"
const lockTag = "operator.1password.io:ignore-secret" const lockTag = "operator.1password.io:ignore-secret"
var log = logf.Log.WithName("update_op_kubernetes_secrets_task") var log = logf.Log.WithName("update_op_kubernetes_secrets_task")
func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler { func NewManager(
kubernetesClient client.Client,
opClient opclient.Client,
shouldAutoRestartDeploymentsGlobal bool,
) *SecretUpdateHandler {
return &SecretUpdateHandler{ return &SecretUpdateHandler{
client: kubernetesClient, client: kubernetesClient,
opConnectClient: opConnectClient, opClient: opClient,
shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal, shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal,
} }
} }
type SecretUpdateHandler struct { type SecretUpdateHandler struct {
client client.Client client client.Client
opConnectClient connect.Client opClient opclient.Client
shouldAutoRestartDeploymentsGlobal bool shouldAutoRestartDeploymentsGlobal bool
} }
func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error { func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask(ctx context.Context) error {
updatedKubernetesSecrets, err := h.updateKubernetesSecrets() updatedKubernetesSecrets, err := h.updateKubernetesSecrets(ctx)
if err != nil { if err != nil {
return err return err
} }
return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets) return h.restartDeploymentsWithUpdatedSecrets(ctx, updatedKubernetesSecrets)
} }
func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret) error { func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(
ctx context.Context,
updatedSecretsByNamespace map[string]map[string]*corev1.Secret,
) error {
// No secrets to update. Exit // No secrets to update. Exit
if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil { if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil {
return nil return nil
} }
deployments := &appsv1.DeploymentList{} deployments := &appsv1.DeploymentList{}
err := h.client.List(context.Background(), deployments) err := h.client.List(ctx, deployments)
if err != nil { if err != nil {
log.Error(err, "Failed to list kubernetes deployments") log.Error(err, "Failed to list kubernetes deployments")
return err return err
@@ -63,7 +70,7 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
return nil return nil
} }
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap() setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -78,32 +85,38 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
} }
for _, secret := range updatedDeploymentSecrets { for _, secret := range updatedDeploymentSecrets {
if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) { if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) {
h.restartDeployment(deployment) h.restartDeployment(ctx, deployment)
continue continue
} }
} }
log.V(logs.DebugLevel).Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace)) log.V(logs.DebugLevel).Info(fmt.Sprintf("Deployment %q at namespace %q is up to date",
deployment.GetName(), deployment.Namespace,
))
} }
return nil return nil
} }
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) { func (h *SecretUpdateHandler) restartDeployment(ctx context.Context, deployment *appsv1.Deployment) {
log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace)) log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting",
deployment.GetName(), deployment.Namespace,
))
if deployment.Spec.Template.Annotations == nil { if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = map[string]string{} deployment.Spec.Template.Annotations = map[string]string{}
} }
deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String() deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String()
err := h.client.Update(context.Background(), deployment) err := h.client.Update(ctx, deployment)
if err != nil { if err != nil {
log.Error(err, "Problem restarting deployment") log.Error(err, "Problem restarting deployment")
} }
} }
func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*corev1.Secret, error) { func (h *SecretUpdateHandler) updateKubernetesSecrets(ctx context.Context) (
map[string]map[string]*corev1.Secret, error,
) {
secrets := &corev1.SecretList{} secrets := &corev1.SecretList{}
err := h.client.List(context.Background(), secrets) err := h.client.List(ctx, secrets)
if err != nil { if err != nil {
log.Error(err, "Failed to list kubernetes secrets") log.Error(err, "Failed to list kubernetes secrets")
return nil, err return nil, err
@@ -121,22 +134,28 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
OnePasswordItemPath := h.getPathFromOnePasswordItem(secret) OnePasswordItemPath := h.getPathFromOnePasswordItem(secret)
item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath) item, err := GetOnePasswordItemByPath(ctx, h.opClient, OnePasswordItemPath)
if err != nil { if err != nil {
log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name) log.Error(err, fmt.Sprintf("failed to retrieve 1Password item at path %s for secret %s",
secret.Annotations[ItemPathAnnotation], secret.Name,
))
continue continue
} }
itemVersion := fmt.Sprint(item.Version) itemVersion := fmt.Sprint(item.Version)
itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID) itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID)
if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString { if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString {
if isItemLockedForForcedRestarts(item) { if isItemLockedForForcedRestarts(item) {
log.V(logs.DebugLevel).Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName())) log.V(logs.DebugLevel).Info(fmt.Sprintf(
"Secret '%v' has been updated in 1Password but is set to be ignored. "+
"Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.",
secret.GetName(),
))
secret.Annotations[VersionAnnotation] = itemVersion secret.Annotations[VersionAnnotation] = itemVersion
secret.Annotations[ItemPathAnnotation] = itemPathString secret.Annotations[ItemPathAnnotation] = itemPathString
if err := h.client.Update(context.Background(), &secret); err != nil { if err := h.client.Update(ctx, &secret); err != nil {
log.Error(err, "failed to update secret %s annotations to version %d: %s", secret.Name, itemVersion, err) log.Error(err, fmt.Sprintf("failed to update secret %s annotations to version %s", secret.Name, itemVersion))
continue continue
} }
continue continue
@@ -145,9 +164,11 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
secret.Annotations[VersionAnnotation] = itemVersion secret.Annotations[VersionAnnotation] = itemVersion
secret.Annotations[ItemPathAnnotation] = itemPathString secret.Annotations[ItemPathAnnotation] = itemPathString
secret.Data = kubeSecrets.BuildKubernetesSecretData(item.Fields, item.Files) secret.Data = kubeSecrets.BuildKubernetesSecretData(item.Fields, item.Files)
log.V(logs.DebugLevel).Info(fmt.Sprintf("New secret path: %v and version: %v", secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation])) log.V(logs.DebugLevel).Info(fmt.Sprintf("New secret path: %v and version: %v",
if err := h.client.Update(context.Background(), &secret); err != nil { secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation],
log.Error(err, "failed to update secret %s to version %d: %s", secret.Name, itemVersion, err) ))
if err := h.client.Update(ctx, &secret); err != nil {
log.Error(err, fmt.Sprintf("failed to update secret %s to version %s", secret.Name, itemVersion))
continue continue
} }
if updatedSecrets[secret.Namespace] == nil { if updatedSecrets[secret.Namespace] == nil {
@@ -159,7 +180,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
return updatedSecrets, nil return updatedSecrets, nil
} }
func isItemLockedForForcedRestarts(item *onepassword.Item) bool { func isItemLockedForForcedRestarts(item *model.Item) bool {
tags := item.Tags tags := item.Tags
for i := 0; i < len(tags); i++ { for i := 0; i < len(tags); i++ {
if tags[i] == lockTag { if tags[i] == lockTag {
@@ -171,15 +192,12 @@ func isItemLockedForForcedRestarts(item *onepassword.Item) bool {
func isUpdatedSecret(secretName string, updatedSecrets map[string]*corev1.Secret) bool { func isUpdatedSecret(secretName string, updatedSecrets map[string]*corev1.Secret) bool {
_, ok := updatedSecrets[secretName] _, ok := updatedSecrets[secretName]
if ok { return ok
return true
}
return false
} }
func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string]bool, error) { func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap(ctx context.Context) (map[string]bool, error) {
namespaces := &corev1.NamespaceList{} namespaces := &corev1.NamespaceList{}
err := h.client.List(context.Background(), namespaces) err := h.client.List(ctx, namespaces)
if err != nil { if err != nil {
log.Error(err, "Failed to list kubernetes namespaces") log.Error(err, "Failed to list kubernetes namespaces")
return nil, err return nil, err
@@ -209,7 +227,11 @@ func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) s
return secret.Annotations[ItemPathAnnotation] return secret.Annotations[ItemPathAnnotation]
} }
func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { func isSecretSetForAutoRestart(
secret *corev1.Secret,
deployment *appsv1.Deployment,
setForAutoRestartByNamespace map[string]bool,
) bool {
restartDeployment := secret.Annotations[RestartDeploymentsAnnotation] restartDeployment := secret.Annotations[RestartDeploymentsAnnotation]
// If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace // If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
if restartDeployment == "" { if restartDeployment == "" {
@@ -218,7 +240,9 @@ func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deploym
restartDeploymentBool, err := utils.StringToBool(restartDeployment) restartDeploymentBool, err := utils.StringToBool(restartDeployment)
if err != nil { if err != nil {
log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secret.Name) log.Error(err, fmt.Sprintf("Error parsing %s annotation on Secret %s. Must be true or false. Defaulting to false.",
RestartDeploymentsAnnotation, secret.Name,
))
return false return false
} }
return restartDeploymentBool return restartDeploymentBool
@@ -233,7 +257,10 @@ func isDeploymentSetForAutoRestart(deployment *appsv1.Deployment, setForAutoRest
restartDeploymentBool, err := utils.StringToBool(restartDeployment) restartDeploymentBool, err := utils.StringToBool(restartDeployment)
if err != nil { if err != nil {
log.Error(err, "Error parsing %v annotation on Deployment %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, deployment.Name) log.Error(err, fmt.Sprintf(
"Error parsing %s annotation on Deployment %s. Must be true or false. Defaulting to false.",
RestartDeploymentsAnnotation, deployment.Name,
))
return false return false
} }
return restartDeploymentBool return restartDeploymentBool
@@ -248,7 +275,9 @@ func (h *SecretUpdateHandler) isNamespaceSetToAutoRestart(namespace *corev1.Name
restartDeploymentBool, err := utils.StringToBool(restartDeployment) restartDeploymentBool, err := utils.StringToBool(restartDeployment)
if err != nil { if err != nil {
log.Error(err, "Error parsing %v annotation on Namespace %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, namespace.Name) log.Error(err, fmt.Sprintf("Error parsing %s annotation on Namespace %s. Must be true or false. Defaulting to false.",
RestartDeploymentsAnnotation, namespace.Name,
))
return false return false
} }
return restartDeploymentBool return restartDeploymentBool

View File

@@ -4,11 +4,14 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/1Password/onepassword-operator/pkg/mocks" "github.com/1Password/onepassword-operator/pkg/mocks"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
errors2 "k8s.io/apimachinery/pkg/api/errors" errors2 "k8s.io/apimachinery/pkg/api/errors"
@@ -40,7 +43,6 @@ type testUpdateSecretTask struct {
existingSecret *corev1.Secret existingSecret *corev1.Secret
expectedError error expectedError error
expectedResultSecret *corev1.Secret expectedResultSecret *corev1.Secret
expectedEvents []string
opItem map[string]string opItem map[string]string
expectedRestart bool expectedRestart bool
globalAutoRestartEnabled bool globalAutoRestartEnabled bool
@@ -60,6 +62,9 @@ var defaultNamespace = &corev1.Namespace{
}, },
} }
// TODO: Refactor test cases to avoid duplication.
//
//nolint:dupl
var tests = []testUpdateSecretTask{ var tests = []testUpdateSecretTask{
{ {
testName: "Test unrelated deployment is not restarted with an updated secret", testName: "Test unrelated deployment is not restarted with an updated secret",
@@ -784,7 +789,7 @@ var tests = []testUpdateSecretTask{
func TestUpdateSecretHandler(t *testing.T) { func TestUpdateSecretHandler(t *testing.T) {
for _, testData := range tests { for _, testData := range tests {
t.Run(testData.testName, func(t *testing.T) { t.Run(testData.testName, func(t *testing.T) {
ctx := context.Background()
// Register operator types with the runtime scheme. // Register operator types with the runtime scheme.
s := scheme.Scheme s := scheme.Scheme
s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.existingDeployment) s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.existingDeployment)
@@ -802,23 +807,15 @@ func TestUpdateSecretHandler(t *testing.T) {
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
opConnectClient := &mocks.TestClient{} mockOpClient := &mocks.TestClient{}
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { mockOpClient.On("GetItemByID", mock.Anything, mock.Anything).Return(createItem(), nil)
item := onepassword.Item{}
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
item.Version = itemVersion
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
h := &SecretUpdateHandler{ h := &SecretUpdateHandler{
client: cl, client: cl,
opConnectClient: opConnectClient, opClient: mockOpClient,
shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled, shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled,
} }
err := h.UpdateKubernetesSecretsTask() err := h.UpdateKubernetesSecretsTask(ctx)
assert.Equal(t, testData.expectedError, err) assert.Equal(t, testData.expectedError, err)
@@ -831,7 +828,7 @@ func TestUpdateSecretHandler(t *testing.T) {
// Check if Secret has been created and has the correct data // Check if Secret has been created and has the correct data
secret := &corev1.Secret{} secret := &corev1.Secret{}
err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret) err = cl.Get(ctx, types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret)
if testData.expectedResultSecret == nil { if testData.expectedResultSecret == nil {
assert.Error(t, err) assert.Error(t, err)
@@ -845,7 +842,8 @@ func TestUpdateSecretHandler(t *testing.T) {
// check if deployment has been restarted // check if deployment has been restarted
deployment := &appsv1.Deployment{} deployment := &appsv1.Deployment{}
err = cl.Get(context.TODO(), types.NamespacedName{Name: testData.existingDeployment.Name, Namespace: namespace}, deployment) err = cl.Get(ctx, types.NamespacedName{Name: testData.existingDeployment.Name, Namespace: namespace}, deployment)
assert.NoError(t, err)
_, ok := deployment.Spec.Template.Annotations[RestartAnnotation] _, ok := deployment.Spec.Template.Annotations[RestartAnnotation]
if ok { if ok {
@@ -854,7 +852,7 @@ func TestUpdateSecretHandler(t *testing.T) {
assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.") assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.")
} }
oldPodTemplateAnnotations := testData.existingDeployment.Spec.Template.ObjectMeta.Annotations oldPodTemplateAnnotations := testData.existingDeployment.Spec.Template.Annotations
newPodTemplateAnnotations := deployment.Spec.Template.Annotations newPodTemplateAnnotations := deployment.Spec.Template.Annotations
for name, expected := range oldPodTemplateAnnotations { for name, expected := range oldPodTemplateAnnotations {
actual, ok := newPodTemplateAnnotations[name] actual, ok := newPodTemplateAnnotations[name]
@@ -879,8 +877,13 @@ func TestIsUpdatedSecret(t *testing.T) {
assert.True(t, isUpdatedSecret(secretName, updatedSecrets)) assert.True(t, isUpdatedSecret(secretName, updatedSecrets))
} }
func generateFields(username, password string) []*onepassword.ItemField { func createItem() *model.Item {
fields := []*onepassword.ItemField{ return &model.Item{
ID: itemId,
VaultID: vaultId,
Version: itemVersion,
Tags: []string{"tag1", "tag2"},
Fields: []model.ItemField{
{ {
Label: "username", Label: "username",
Value: username, Value: username,
@@ -889,6 +892,8 @@ func generateFields(username, password string) []*onepassword.ItemField {
Label: "password", Label: "password",
Value: password, Value: password,
}, },
},
Files: []model.File{},
CreatedAt: time.Now(),
} }
return fields
} }

View File

@@ -10,13 +10,14 @@ func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]*corev1.
return false return false
} }
} }
if len(volumes) == 0 { return len(volumes) > 0
return false
}
return true
} }
func AppendUpdatedVolumeSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { func AppendUpdatedVolumeSecrets(
volumes []corev1.Volume,
secrets map[string]*corev1.Secret,
updatedDeploymentSecrets map[string]*corev1.Secret,
) map[string]*corev1.Secret {
for i := 0; i < len(volumes); i++ { for i := 0; i < len(volumes); i++ {
secret := IsVolumeUsingSecret(volumes[i], secrets) secret := IsVolumeUsingSecret(volumes[i], secrets)
if secret != nil { if secret != nil {

View File

@@ -0,0 +1,8 @@
package defaults
import "time"
const (
E2EInterval = 1 * time.Second
E2ETimeout = 1 * time.Minute
)

View File

@@ -0,0 +1,23 @@
package kind
import (
"os"
//nolint:staticcheck // ST1001
. "github.com/onsi/ginkgo/v2"
//nolint:staticcheck // ST1001
. "github.com/onsi/gomega"
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
)
// LoadImageToKind loads a local docker image to the Kind cluster
func LoadImageToKind(imageName string) {
By("Loading the operator image on Kind")
clusterName := "kind"
if value, ok := os.LookupEnv("KIND_CLUSTER"); ok {
clusterName = value
}
_, err := system.Run("kind", "load", "docker-image", imageName, "--name", clusterName)
Expect(err).NotTo(HaveOccurred())
}

View File

@@ -0,0 +1,125 @@
package kube
import (
"context"
"time"
//nolint:staticcheck // ST1001
. "github.com/onsi/ginkgo/v2"
//nolint:staticcheck // ST1001
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type Deployment struct {
client client.Client
config *Config
name string
}
func (d *Deployment) Get(ctx context.Context) *appsv1.Deployment {
// Derive a short-lived context so this API call won't hang indefinitely.
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
deployment := &appsv1.Deployment{}
err := d.client.Get(c, client.ObjectKey{Name: d.name, Namespace: d.config.Namespace}, deployment)
Expect(err).ToNot(HaveOccurred())
return deployment
}
func (d *Deployment) ReadEnvVar(ctx context.Context, envVarName string) string {
By("Reading " + envVarName + " value from deployment/" + d.name)
deployment := d.Get(ctx)
// Search env across all containers
found := ""
for _, container := range deployment.Spec.Template.Spec.Containers {
for _, env := range container.Env {
if env.Name == envVarName && env.Value != "" {
found = env.Value
break
}
}
}
Expect(found).NotTo(BeEmpty())
return found
}
func (d *Deployment) PatchEnvVars(ctx context.Context, upsert []corev1.EnvVar, remove []string) {
By("Patching env variables for deployment/" + d.name)
deployment := d.Get(ctx)
deploymentCopy := deployment.DeepCopy()
container := &deployment.Spec.Template.Spec.Containers[0]
// Build removal set for quick lookup
toRemove := make(map[string]struct{}, len(remove))
for _, n := range remove {
toRemove[n] = struct{}{}
}
// Build upsert map for quick lookup
upserts := make(map[string]corev1.EnvVar, len(upsert))
for _, e := range upsert {
upserts[e.Name] = e
}
// Filter existing envs: keep if not in remove and not being upserted
filtered := make([]corev1.EnvVar, 0, len(container.Env))
for _, e := range container.Env {
if _, ok := toRemove[e.Name]; ok {
continue
}
if newE, ok := upserts[e.Name]; ok {
filtered = append(filtered, newE) // replace existing
delete(upserts, e.Name) // delete from map to not use once again
} else {
filtered = append(filtered, e)
}
}
// Append any new envs that werent already in the container
for _, e := range upserts {
filtered = append(filtered, e)
}
container.Env = filtered
// Derive a short-lived context so this API call won't hang indefinitely.
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
err := d.client.Patch(c, deployment, client.MergeFrom(deploymentCopy))
Expect(err).ToNot(HaveOccurred())
// wait for new deployment to roll out
d.WaitDeploymentRolledOut(ctx)
}
// WaitDeploymentRolledOut waits for deployment to finish a rollout.
func (d *Deployment) WaitDeploymentRolledOut(ctx context.Context) {
By("Waiting for deployment/" + d.name + " to roll out")
deployment := d.Get(ctx)
targetGen := deployment.Generation
Eventually(func(g Gomega) error {
newDeployment := d.Get(ctx)
g.Expect(newDeployment.Status.ObservedGeneration).To(BeNumerically(">=", targetGen))
desired := int32(1)
if newDeployment.Spec.Replicas != nil {
desired = *newDeployment.Spec.Replicas
}
g.Expect(newDeployment.Status.UpdatedReplicas).To(Equal(desired))
g.Expect(newDeployment.Status.AvailableReplicas).To(Equal(desired))
g.Expect(newDeployment.Status.Replicas).To(Equal(desired))
return nil
}, d.config.TestConfig.Timeout, d.config.TestConfig.Interval).Should(Succeed())
}

224
pkg/testhelper/kube/kube.go Normal file
View File

@@ -0,0 +1,224 @@
package kube
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
//nolint:staticcheck // ST1001
. "github.com/onsi/ginkgo/v2"
//nolint:staticcheck // ST1001
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
apiv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
)
type TestConfig struct {
Timeout time.Duration
Interval time.Duration
}
type Config struct {
Namespace string
ManifestsDir string
TestConfig *TestConfig
CRDs []string
}
type Kube struct {
Config *Config
Client client.Client
Mapper meta.RESTMapper
}
func NewKubeClient(config *Config) *Kube {
By("Creating a kubernetes client")
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
home, _ := os.UserHomeDir()
kubeconfig = filepath.Join(home, ".kube", "config")
}
restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
Expect(err).NotTo(HaveOccurred())
// Install CRDs first (so discovery sees them)
installCRDs(context.Background(), restConfig, config.CRDs)
// Build an http.Client from restConfig
httpClient, err := rest.HTTPClientFor(restConfig)
Expect(err).NotTo(HaveOccurred())
// Create a Dynamic RESTMapper that uses restConfig
rm, err := apiutil.NewDynamicRESTMapper(restConfig, httpClient)
Expect(err).NotTo(HaveOccurred())
scheme := runtime.NewScheme()
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(appsv1.AddToScheme(scheme))
utilruntime.Must(apiv1.AddToScheme(scheme)) // add OnePasswordItem to scheme
kubernetesClient, err := client.New(restConfig, client.Options{
Scheme: scheme,
Mapper: rm,
})
Expect(err).NotTo(HaveOccurred())
// update the current contexts namespace in kubeconfig
pathOpts := clientcmd.NewDefaultPathOptions()
cfg, err := pathOpts.GetStartingConfig()
Expect(err).NotTo(HaveOccurred())
currentContext := cfg.CurrentContext
Expect(currentContext).NotTo(BeEmpty(), "no current kube context is set in kubeconfig")
ctx, ok := cfg.Contexts[currentContext]
Expect(ok).To(BeTrue(), fmt.Sprintf("current context %q not found in kubeconfig", currentContext))
ctx.Namespace = config.Namespace
err = clientcmd.ModifyConfig(pathOpts, *cfg, true)
Expect(err).NotTo(HaveOccurred())
return &Kube{
Config: config,
Client: kubernetesClient,
Mapper: rm,
}
}
func (k *Kube) Secret(name string) *Secret {
return &Secret{
client: k.Client,
config: k.Config,
name: name,
}
}
func (k *Kube) Deployment(name string) *Deployment {
return &Deployment{
client: k.Client,
config: k.Config,
name: name,
}
}
func (k *Kube) Pod(selector map[string]string) *Pod {
return &Pod{
client: k.Client,
config: k.Config,
selector: selector,
}
}
func (k *Kube) Namespace(name string) *Namespace {
return &Namespace{
client: k.Client,
config: k.Config,
name: name,
}
}
// ApplyOnePasswordItem applies a OnePasswordItem manifest.
func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) {
By("Applying " + fileName)
// Derive a short-lived context so this API call won't hang indefinitely.
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
data, err := os.ReadFile(k.Config.ManifestsDir + "/" + fileName)
Expect(err).NotTo(HaveOccurred())
// Decode YAML -> JSON -> unstructured.Unstructured
jsonBytes, err := yaml.ToJSON(data)
Expect(err).NotTo(HaveOccurred())
var obj unstructured.Unstructured
Expect(obj.UnmarshalJSON(jsonBytes)).To(Succeed())
// Default namespace for namespaced resources if not set in YAML
if obj.GetNamespace() == "" && k.Config.Namespace != "" {
gvk := obj.GroupVersionKind()
mapping, mapErr := k.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if mapErr == nil && mapping.Scope.Name() == meta.RESTScopeNameNamespace {
obj.SetNamespace(k.Config.Namespace)
}
}
// Server-Side Apply (create or update)
patchOpts := []client.PatchOption{
client.FieldOwner("onepassword-e2e"),
client.ForceOwnership, // to force-take conflicting fields
}
Expect(k.Client.Patch(c, &obj, client.Apply, patchOpts...)).To(Succeed())
}
func installCRDs(ctx context.Context, restConfig *rest.Config, crdFiles []string) {
apixClient, err := apix.NewForConfig(restConfig)
Expect(err).NotTo(HaveOccurred())
for _, f := range crdFiles {
By("Installing CRD " + f)
b, err := os.ReadFile(filepath.Clean(f))
Expect(err).NotTo(HaveOccurred())
var crd apiextv1.CustomResourceDefinition
err = yaml.Unmarshal(b, &crd)
Expect(err).NotTo(HaveOccurred())
// Create or Update
_, err = apixClient.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, &crd, metav1.CreateOptions{})
if apierrors.IsAlreadyExists(err) {
existing, getErr := apixClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Name, metav1.GetOptions{})
Expect(getErr).NotTo(HaveOccurred())
crd.ResourceVersion = existing.ResourceVersion
_, err = apixClient.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, &crd, metav1.UpdateOptions{})
}
Expect(err).NotTo(HaveOccurred())
waitCRDEstablished(ctx, apixClient, crd.Name)
}
}
// waitCRDEstablished Wait until the CRD reaches Established=True, retrying until the suite timeout.
func waitCRDEstablished(ctx context.Context, apixClient *apix.Clientset, name string) {
By("Waiting for CRD " + name + " to be Established")
Eventually(func(g Gomega) {
// Short per-attempt timeout so a single Get can't hang the whole Eventually loop.
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
crd, err := apixClient.ApiextensionsV1().
CustomResourceDefinitions().
Get(attemptCtx, name, metav1.GetOptions{})
g.Expect(err).NotTo(HaveOccurred())
established := false
for _, c := range crd.Status.Conditions {
if c.Type == apiextv1.Established && c.Status == apiextv1.ConditionTrue {
established = true
break
}
}
g.Expect(established).To(BeTrue(), "CRD %q is not Established yet", name)
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
}

View File

@@ -0,0 +1,41 @@
package kube
import (
"context"
//nolint:staticcheck // ST1001
. "github.com/onsi/ginkgo/v2"
//nolint:staticcheck // ST1001
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type Namespace struct {
client client.Client
config *Config
name string
}
// LabelNamespace applies the given labels to the specified namespace
func (n *Namespace) LabelNamespace(ctx context.Context, labelsMap map[string]string) {
if len(labelsMap) == 0 {
return
}
By("Setting labelsMap " + labels.Set(labelsMap).String() + " to namespace/" + n.name)
ns := &corev1.Namespace{}
err := n.client.Get(ctx, client.ObjectKey{Name: n.name}, ns)
Expect(err).NotTo(HaveOccurred())
if ns.Labels == nil {
ns.Labels = map[string]string{}
}
for k, v := range labelsMap {
ns.Labels[k] = v
}
err = n.client.Update(ctx, ns)
Expect(err).NotTo(HaveOccurred())
}

View File

@@ -0,0 +1,47 @@
package kube
import (
"context"
"time"
//nolint:staticcheck // ST1001
. "github.com/onsi/ginkgo/v2"
//nolint:staticcheck // ST1001
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type Pod struct {
client client.Client
config *Config
selector map[string]string
}
func (p *Pod) WaitingForRunningPod(ctx context.Context) {
By("Waiting for the pod " + labels.Set(p.selector).String() + " to be 'Running'")
Eventually(func(g Gomega) {
// short per-attempt timeout to avoid hanging calls while Eventually polls
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
var pods corev1.PodList
listOpts := []client.ListOption{
client.InNamespace(p.config.Namespace),
client.MatchingLabels(p.selector),
}
g.Expect(p.client.List(attemptCtx, &pods, listOpts...)).To(Succeed())
g.Expect(pods.Items).NotTo(BeEmpty(), "no pods found with selector %q", labels.Set(p.selector).String())
foundRunning := false
for _, p := range pods.Items {
if p.Status.Phase == corev1.PodRunning {
foundRunning = true
break
}
}
g.Expect(foundRunning).To(BeTrue(), "pod not Running yet")
}, p.config.TestConfig.Timeout, p.config.TestConfig.Interval).Should(Succeed())
}

View File

@@ -0,0 +1,141 @@
package kube
import (
"context"
"encoding/base64"
"os"
"path/filepath"
"time"
//nolint:staticcheck // ST1001
. "github.com/onsi/ginkgo/v2"
//nolint:staticcheck // ST1001
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
)
type Secret struct {
client client.Client
config *Config
name string
}
// CreateFromEnvVar creates a kubernetes secret from an environment variable
func (s *Secret) CreateFromEnvVar(ctx context.Context, envVar string) *corev1.Secret {
By("Creating '" + s.name + "' secret from environment variable")
// Derive a short-lived context so this API call won't hang indefinitely.
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
value, ok := os.LookupEnv(envVar)
Expect(ok).To(BeTrue())
Expect(value).NotTo(BeEmpty())
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: s.name,
Namespace: s.config.Namespace,
},
StringData: map[string]string{
"token": value,
},
}
err := s.client.Create(c, secret)
Expect(err).NotTo(HaveOccurred())
return secret
}
// CreateFromFile creates a kubernetes secret from a file
func (s *Secret) CreateFromFile(ctx context.Context, fileName string, content []byte) *corev1.Secret {
By("Creating '" + s.name + "' secret from file " + fileName)
// Derive a short-lived context so this API call won't hang indefinitely.
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: s.name,
Namespace: s.config.Namespace,
},
Data: map[string][]byte{
filepath.Base(fileName): content,
},
}
err := s.client.Create(c, secret)
Expect(err).NotTo(HaveOccurred())
return secret
}
// CreateOpCredentials creates a kubernetes secret from 1password-credentials.json file in the project root
// encodes it in base64 and saves it to op-session file
func (s *Secret) CreateOpCredentials(ctx context.Context) *corev1.Secret {
rootDir, err := system.GetProjectRoot()
Expect(err).NotTo(HaveOccurred())
credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json")
data, err := os.ReadFile(credentialsFilePath)
Expect(err).NotTo(HaveOccurred())
encoded := base64.RawURLEncoding.EncodeToString(data)
return s.CreateFromFile(ctx, "op-session", []byte(encoded))
}
// Get retrieves a kubernetes secret
func (s *Secret) Get(ctx context.Context) *corev1.Secret {
By("Getting '" + s.name + "' secret")
// Derive a short-lived context so this API call won't hang indefinitely.
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
secret := &corev1.Secret{}
err := s.client.Get(c, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret)
Expect(err).NotTo(HaveOccurred())
return secret
}
// Delete deletes a kubernetes secret
func (s *Secret) Delete(ctx context.Context) {
By("Deleting '" + s.name + "' secret")
// Derive a short-lived context so this API call won't hang indefinitely.
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: s.name,
Namespace: s.config.Namespace,
},
}
err := s.client.Delete(c, secret)
Expect(err).NotTo(HaveOccurred())
}
// CheckIfExists repeatedly attempts to retrieve the given Secret
// from the cluster until it is found or the test's timeout expires.
func (s *Secret) CheckIfExists(ctx context.Context) {
By("Checking '" + s.name + "' secret")
Eventually(func(g Gomega) {
// Derive a short-lived context so this API call won't hang indefinitely.
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
secret := &corev1.Secret{}
err := s.client.Get(attemptCtx, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret)
g.Expect(err).NotTo(HaveOccurred())
}, s.config.TestConfig.Timeout, s.config.TestConfig.Interval).Should(Succeed())
}

32
pkg/testhelper/op/op.go Normal file
View File

@@ -0,0 +1,32 @@
package op
import (
"fmt"
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
)
type Field string
const (
FieldUsername = "username"
FieldPassword = "password"
)
// UpdateItemPassword updates the password of an item in 1Password
func UpdateItemPassword(item string) error {
_, err := system.Run("op", "item", "edit", item, "--generate-password=letters,digits,symbols,32")
if err != nil {
return err
}
return nil
}
// ReadItemField reads the password of an item in 1Password
func ReadItemField(item, vault string, field Field) (string, error) {
output, err := system.Run("op", "read", fmt.Sprintf("op://%s/%s/%s", vault, item, field))
if err != nil {
return "", err
}
return output, nil
}

View File

@@ -0,0 +1,94 @@
package system
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Run executes the provided command within this context
func Run(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
rootDir, err := GetProjectRoot()
if err != nil {
return "", err
}
// Command will run from project root
cmd.Dir = rootDir
command := strings.Join(cmd.Args, " ")
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output))
}
return string(output), nil
}
func GetProjectRoot() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
// check if go.mod exists in current dir
modFile := filepath.Join(dir, "go.mod")
if _, err := os.Stat(modFile); err == nil {
return dir, nil
}
// move one level up
parent := filepath.Dir(dir)
if parent == dir {
// reached filesystem root
return "", fmt.Errorf("project root not found (no go.mod)")
}
dir = parent
}
}
func ReplaceFile(src, dst string) error {
rootDir, err := GetProjectRoot()
if err != nil {
return err
}
// Open the source file
sourceFile, err := os.Open(filepath.Join(rootDir, src))
if err != nil {
return err
}
defer func(sourceFile *os.File) {
cerr := sourceFile.Close()
if err != nil {
err = errors.Join(err, cerr)
}
}(sourceFile)
// Create (or overwrite) the destination file
destFile, err := os.Create(filepath.Join(rootDir, dst))
if err != nil {
return err
}
defer func(destFile *os.File) {
cerr := destFile.Close()
if err != nil {
err = errors.Join(err, cerr)
}
}(destFile)
// Copy contents
if _, err = io.Copy(destFile, sourceFile); err != nil {
return err
}
// Ensure data is written to disk
return destFile.Sync()
}

View File

@@ -0,0 +1,14 @@
package e2e
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// Run e2e tests using the Ginkgo runner.
func TestE2E(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "onepassword-operator e2e suite")
}

233
test/e2e/e2e_test.go Normal file
View File

@@ -0,0 +1,233 @@
package e2e
import (
"context"
"path/filepath"
"strconv"
"time"
//nolint:staticcheck // ST1001
. "github.com/onsi/ginkgo/v2"
//nolint:staticcheck // ST1001
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
"github.com/1Password/onepassword-operator/pkg/testhelper/kind"
"github.com/1Password/onepassword-operator/pkg/testhelper/kube"
"github.com/1Password/onepassword-operator/pkg/testhelper/op"
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
)
const (
operatorImageName = "1password/onepassword-operator:latest"
vaultName = "operator-acceptance-tests"
)
var kubeClient *kube.Kube
var _ = Describe("Onepassword Operator e2e", Ordered, func() {
ctx := context.Background()
BeforeAll(func() {
rootDir, err := system.GetProjectRoot()
Expect(err).NotTo(HaveOccurred())
kubeClient = kube.NewKubeClient(&kube.Config{
Namespace: "default",
ManifestsDir: filepath.Join("manifests"),
TestConfig: &kube.TestConfig{
Timeout: defaults.E2ETimeout,
Interval: defaults.E2EInterval,
},
CRDs: []string{
filepath.Join(rootDir, "config", "crd", "bases", "onepassword.com_onepassworditems.yaml"),
},
})
By("Building the Operator image")
_, err = system.Run("make", "docker-build")
Expect(err).NotTo(HaveOccurred())
kind.LoadImageToKind(operatorImageName)
kubeClient.Secret("op-credentials").CreateOpCredentials(ctx)
kubeClient.Secret("op-credentials").CheckIfExists(ctx)
kubeClient.Secret("onepassword-token").CreateFromEnvVar(ctx, "OP_CONNECT_TOKEN")
kubeClient.Secret("onepassword-token").CheckIfExists(ctx)
kubeClient.Secret("onepassword-service-account-token").CreateFromEnvVar(ctx, "OP_SERVICE_ACCOUNT_TOKEN")
kubeClient.Secret("onepassword-service-account-token").CheckIfExists(ctx)
By("Replace manager.yaml")
err = system.ReplaceFile("test/e2e/manifests/manager.yaml", "config/manager/manager.yaml")
Expect(err).NotTo(HaveOccurred())
_, err = system.Run("make", "deploy")
Expect(err).NotTo(HaveOccurred())
kubeClient.Pod(map[string]string{"name": "onepassword-connect-operator"}).WaitingForRunningPod(ctx)
})
Context("Use the operator with Service Account", func() {
runCommonTestCases(ctx)
})
Context("Use the operator with Connect", func() {
BeforeAll(func() {
kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{
{Name: "MANAGE_CONNECT", Value: "true"},
{Name: "OP_CONNECT_HOST", Value: "http://onepassword-connect:8080"},
{
Name: "OP_CONNECT_TOKEN",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "onepassword-token",
},
Key: "token",
},
},
},
}, []string{"OP_SERVICE_ACCOUNT_TOKEN"})
kubeClient.Secret("login").Delete(ctx) // remove secret crated in previous test
kubeClient.Secret("secret-ignored").Delete(ctx) // remove secret crated in previous test
kubeClient.Secret("secret-for-update").Delete(ctx) // remove secret crated in previous test
kubeClient.Pod(map[string]string{"app": "onepassword-connect"}).WaitingForRunningPod(ctx)
})
runCommonTestCases(ctx)
})
})
// runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods.
func runCommonTestCases(ctx context.Context) {
It("Should create kubernetes secret from manifest file", func() {
By("Creating secret `login` from 1Password item")
kubeClient.ApplyOnePasswordItem(ctx, "secret.yaml")
kubeClient.Secret("login").CheckIfExists(ctx)
})
It("Kubernetes secret is updated after POOLING_INTERVAL, when updating item in 1Password", func() {
itemName := "secret-for-update"
secretName := itemName
By("Creating secret `" + secretName + "` from 1Password item")
kubeClient.ApplyOnePasswordItem(ctx, secretName+".yaml")
kubeClient.Secret(secretName).CheckIfExists(ctx)
By("Reading old password")
secret := kubeClient.Secret(secretName).Get(ctx)
oldPassword, ok := secret.Data["password"]
Expect(ok).To(BeTrue())
By("Updating `" + secretName + "` 1Password item")
err := op.UpdateItemPassword(itemName)
Expect(err).NotTo(HaveOccurred())
// checking that password was updated
Eventually(func(g Gomega) {
// Derive a short-lived context so this API call won't hang indefinitely.
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
secret = kubeClient.Secret(secretName).Get(attemptCtx)
g.Expect(err).NotTo(HaveOccurred())
newPassword, ok := secret.Data["password"]
g.Expect(ok).To(BeTrue())
g.Expect(newPassword).NotTo(Equal(oldPassword))
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
})
It("1Password item with `ignore-secret` tag doesn't pull updates to kubernetes secret", func() {
itemName := "secret-ignored"
secretName := itemName
By("Creating secret `" + secretName + "` from 1Password item")
kubeClient.ApplyOnePasswordItem(ctx, secretName+".yaml")
kubeClient.Secret(secretName).CheckIfExists(ctx)
By("Reading old password")
secret := kubeClient.Secret(secretName).Get(ctx)
oldPassword, ok := secret.Data["password"]
Expect(ok).To(BeTrue())
By("Updating `" + secretName + "` 1Password item")
err := op.UpdateItemPassword(itemName)
Expect(err).NotTo(HaveOccurred())
newPassword, err := op.ReadItemField(itemName, vaultName, op.FieldPassword)
Expect(err).NotTo(HaveOccurred())
Expect(newPassword).NotTo(BeEquivalentTo(oldPassword))
// checking that password was NOT updated
Eventually(func(g Gomega) {
// Derive a short-lived context so this API call won't hang indefinitely.
attemptCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
intervalStr := kubeClient.Deployment("onepassword-connect-operator").ReadEnvVar(attemptCtx, "POLLING_INTERVAL")
Expect(intervalStr).NotTo(BeEmpty())
i, err := strconv.Atoi(intervalStr)
Expect(err).NotTo(HaveOccurred())
// convert to duration in seconds
interval := time.Duration(i) * time.Second
// wait for one polling interval + 2 seconds to make sure updated secret is pulled
time.Sleep(interval + 2*time.Second)
secret = kubeClient.Secret(secretName).Get(attemptCtx)
g.Expect(err).NotTo(HaveOccurred())
currentPassword, ok := secret.Data["password"]
Expect(ok).To(BeTrue())
Expect(currentPassword).To(BeEquivalentTo(oldPassword))
Expect(currentPassword).NotTo(BeEquivalentTo(newPassword))
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
})
It("AUTO_RESTART restarts deployments using 1Password secrets after item update", func() {
By("Enabling AUTO_RESTART")
kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{
{Name: "AUTO_RESTART", Value: "true"},
}, nil)
DeferCleanup(func() {
By("Disabling AUTO_RESTART")
// disable AUTO_RESTART after test
kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{
{Name: "AUTO_RESTART", Value: "false"},
}, nil)
})
// Ensure the secret exists (created in earlier test), but apply again safely just in case
kubeClient.ApplyOnePasswordItem(ctx, "secret-for-update.yaml")
kubeClient.Secret("secret-for-update").CheckIfExists(ctx)
// add custom secret to the operator
kubeClient.Deployment("onepassword-connect-operator").PatchEnvVars(ctx, []corev1.EnvVar{
{
Name: "CUSTOM_SECRET",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "secret-for-update",
},
Key: "password",
},
},
},
}, nil)
By("Updating `secret-for-update` 1Password item")
err := op.UpdateItemPassword("secret-for-update")
Expect(err).NotTo(HaveOccurred())
By("Checking the operator is restarted")
kubeClient.Deployment("onepassword-connect-operator").WaitDeploymentRolledOut(ctx)
})
}

View File

@@ -0,0 +1,99 @@
# This manager file is used for e2e tests.
# It will be copied to `config/manager` and be used when deploying the operator in e2e tests
# The purpose of it is to increase e2e tests speed and do not introduce additional changes in original `manager.yaml`
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:
securityContext:
runAsNonRoot: true
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_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
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
serviceAccountName: onepassword-connect-operator
terminationGracePeriodSeconds: 10

View File

@@ -0,0 +1,6 @@
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: secret-for-update
spec:
itemPath: "vaults/operator-acceptance-tests/items/secret-for-update"

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