mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 07:28:06 +00:00
Compare commits
148 Commits
v1.8.0
...
6baef1b9cf
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6baef1b9cf | ||
![]() |
7e08158d2f | ||
![]() |
976909c438 | ||
![]() |
e61ba49018 | ||
![]() |
6492b3cf34 | ||
![]() |
9d08bcc864 | ||
![]() |
f7f5462133 | ||
![]() |
a1cbd40f9e | ||
![]() |
d75a33d524 | ||
![]() |
b1b6c97a88 | ||
![]() |
0c3caf88b6 | ||
![]() |
24edff22d4 | ||
![]() |
8c893270f4 | ||
![]() |
d5f1044571 | ||
![]() |
b40f27b052 | ||
![]() |
cd03a651ad | ||
![]() |
9aac824066 | ||
![]() |
05ad484bd6 | ||
![]() |
71b29d5fe6 | ||
![]() |
c082f9562e | ||
![]() |
57478247cf | ||
![]() |
4836140f66 | ||
![]() |
2b36f16940 | ||
![]() |
bb97134e10 | ||
![]() |
904d269e7b | ||
![]() |
cf9b267eaf | ||
![]() |
4d64beab86 | ||
![]() |
ca051a08cf | ||
![]() |
22a7c8f586 | ||
![]() |
2003d13788 | ||
![]() |
7187f41ef1 | ||
![]() |
d0b11c70f0 | ||
![]() |
9825cb57c9 | ||
![]() |
6bb6088353 | ||
![]() |
5a56fd3330 | ||
![]() |
dcd7eefac0 | ||
![]() |
29b7ed7899 | ||
![]() |
331e8d7bfb | ||
![]() |
c144bd3d01 | ||
![]() |
299689fe13 | ||
![]() |
882d8e951d | ||
![]() |
7885ba649b | ||
![]() |
600adf2670 | ||
![]() |
88b2dfbf67 | ||
![]() |
e167db2357 | ||
![]() |
91a9bb6d63 | ||
![]() |
116c8c92a7 | ||
![]() |
4307e9d713 | ||
![]() |
1759055edd | ||
![]() |
c1e9934088 | ||
![]() |
19b629f2ee | ||
![]() |
174f952691 | ||
![]() |
f8704223c8 | ||
![]() |
5630d788a2 | ||
![]() |
d504e5ef35 | ||
![]() |
7d2596a4aa | ||
![]() |
f6b267726d | ||
![]() |
bf8c1291b2 | ||
![]() |
cd504ec7df | ||
![]() |
cabc020cc6 | ||
![]() |
54eed0c81c | ||
![]() |
8bd7d519fe | ||
![]() |
2823a571e9 | ||
![]() |
772e708f02 | ||
![]() |
4deb27b853 | ||
![]() |
75e24e9e47 | ||
![]() |
583b8251d8 | ||
![]() |
285066139f | ||
![]() |
1d613eac2b | ||
![]() |
dbd7408fac | ||
![]() |
6ef0da2d17 | ||
![]() |
b35acb7d13 | ||
![]() |
9cee6595d5 | ||
![]() |
24d3f6f043 | ||
![]() |
5980e7e63a | ||
![]() |
1e9c04ee05 | ||
![]() |
a5416f4532 | ||
![]() |
7e739a6fc7 | ||
![]() |
0f1dcdd38a | ||
![]() |
4c04c6699b | ||
![]() |
cd03176aae | ||
![]() |
f194485a1b | ||
![]() |
d1254b06e7 | ||
![]() |
7c84f9d3a4 | ||
![]() |
13abcb9c8f | ||
![]() |
b717823fd0 | ||
![]() |
c9b969def1 | ||
![]() |
fd92ef86ab | ||
![]() |
842c8febdc | ||
![]() |
49a5e93c44 | ||
![]() |
ac646ec56c | ||
![]() |
458ed24ca3 | ||
![]() |
bb7b565457 | ||
![]() |
17d44d90bd | ||
![]() |
0903bacfbd | ||
![]() |
32360d8a83 | ||
![]() |
2373fbc87f | ||
![]() |
704116b855 | ||
![]() |
55b5781d7a | ||
![]() |
1aa27fdba0 | ||
![]() |
f164a93b72 | ||
![]() |
9d0736285f | ||
![]() |
aa1b4ba857 | ||
![]() |
ae9b673f96 | ||
![]() |
a0475d7170 | ||
![]() |
922f3c8929 | ||
![]() |
1fa5bccec2 | ||
![]() |
cff4d194ba | ||
![]() |
475860a364 | ||
![]() |
64aad3d573 | ||
![]() |
0582c2d6e1 | ||
![]() |
d1be03edd0 | ||
![]() |
83b294690a | ||
![]() |
ef7361b3c1 | ||
![]() |
04c1fc1236 | ||
![]() |
3723c962fe | ||
![]() |
4d2120061d | ||
![]() |
c95078c34c | ||
![]() |
4527336c37 | ||
![]() |
0b6b07b867 | ||
![]() |
4baad12e10 | ||
![]() |
efbe96e93a | ||
![]() |
ac06f8db13 | ||
![]() |
72511ed687 | ||
![]() |
4757263c66 | ||
![]() |
97e06e5c4d | ||
![]() |
a432b42814 | ||
![]() |
f88ea6696b | ||
![]() |
1498c223a5 | ||
![]() |
432f2c6cf6 | ||
![]() |
a49c6ee045 | ||
![]() |
8881782559 | ||
![]() |
dcb5d5675a | ||
![]() |
b567b99774 | ||
![]() |
02c90d424b | ||
![]() |
4428515407 | ||
![]() |
949a840779 | ||
![]() |
ced45c33d4 | ||
![]() |
4091f80351 | ||
![]() |
c94e7a5557 | ||
![]() |
3fbd0b32cd | ||
![]() |
2c55fbc5ed | ||
![]() |
fcb97e1482 | ||
![]() |
b3346cbc25 | ||
![]() |
8c0f1a7837 | ||
![]() |
eda5612827 | ||
![]() |
5f232b121a | ||
![]() |
f72e5243b0 |
25
.devcontainer/devcontainer.json
Normal file
25
.devcontainer/devcontainer.json
Normal 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"
|
||||
}
|
||||
|
23
.devcontainer/post-install.sh
Normal file
23
.devcontainer/post-install.sh
Normal 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
|
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -1,21 +1,22 @@
|
||||
name: Build and Test
|
||||
on: [push, pull_request]
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: Run on Ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ^1.20
|
||||
- name: Clone the code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
|
24
.github/workflows/lint.yml
vendored
Normal file
24
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Run on Ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone the code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Run linter
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.2
|
13
.github/workflows/pr-check-signed-commits.yml
vendored
Normal file
13
.github/workflows/pr-check-signed-commits.yml
vendored
Normal 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
|
4
.github/workflows/release-pr.yml
vendored
4
.github/workflows/release-pr.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
-
|
||||
id: is_release_branch_without_pr
|
||||
name: Find matching PR
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Create Release Pull Request
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Parse release version
|
||||
id: get_version
|
||||
|
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -12,13 +12,13 @@ jobs:
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
1password/onepassword-operator
|
||||
@@ -33,19 +33,19 @@ jobs:
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
|
47
.github/workflows/test-e2e.yml
vendored
Normal file
47
.github/workflows/test-e2e.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Test E2E
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches: ['**'] # run for PRs targeting any branch (main and others)
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.event.pull_request.head.ref }}
|
||||
cancel-in-progress: true # cancel previous job runs for the same branch
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
|
||||
- name: Create kind cluster
|
||||
uses: helm/kind-action@v1
|
||||
with:
|
||||
cluster_name: onepassword-operator-test-e2e
|
||||
|
||||
- name: Install 1Password CLI
|
||||
uses: 1password/install-cli-action@v2
|
||||
with:
|
||||
version: 2.32.0
|
||||
|
||||
- name: Create '1password-credentials.json' file
|
||||
env:
|
||||
OP_CONNECT_CREDENTIALS: ${{ secrets.OP_CONNECT_CREDENTIALS }}
|
||||
run: |
|
||||
echo "$OP_CONNECT_CREDENTIALS" > 1password-credentials.json
|
||||
|
||||
- name: Run E2E tests
|
||||
run: make test-e2e
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
24
.github/workflows/test.yml
vendored
Normal file
24
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run on Ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone the code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Running Tests
|
||||
run: |
|
||||
go mod tidy
|
||||
make test
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -8,14 +8,16 @@
|
||||
bin
|
||||
testbin/*
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.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.*
|
||||
|
||||
# editor and IDE paraphernalia
|
||||
@@ -23,3 +25,6 @@ testbin/*
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
1password-credentials.json
|
||||
op-session
|
||||
|
52
.golangci.yml
Normal file
52
.golangci.yml
Normal 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$
|
32
CHANGELOG.md
32
CHANGELOG.md
@@ -12,6 +12,38 @@
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.9.1)
|
||||
# v1.9.1
|
||||
|
||||
## Fixes
|
||||
* Operator no longer panics when handling 1Password items containing files. {#209}
|
||||
|
||||
## Security
|
||||
* HTTP Proxy bypass using IPv6 Zone IDs in golang.org/x/net. {#210}
|
||||
* golang.org/x/net vulnerable to Cross-site Scripting. {#210}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.9.0)
|
||||
# v1.9.0
|
||||
|
||||
## Features
|
||||
* Enable the Operator to authenticate to 1Password using service accounts. {#160}
|
||||
|
||||
## Fixes
|
||||
* Update Operator to use SDK v1.34.1. {#185}
|
||||
* Pass Kubernetes context down to SDK/Connect. {#199}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.8.1)
|
||||
# v1.8.1
|
||||
|
||||
## Fixes
|
||||
* Upgrade operator to use Operator SDK v1.33.0. {#180}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.8.0)
|
||||
# v1.8.0
|
||||
|
||||
|
88
CONTRIBUTING.md
Normal file
88
CONTRIBUTING.md
Normal 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)
|
18
Dockerfile
18
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.20 as builder
|
||||
FROM golang:1.24 AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -7,29 +7,29 @@ WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
# cache deps before building and copying source so that we don't need to re-download as much
|
||||
# and so that source changes don't invalidate our downloaded layer
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy the go source
|
||||
COPY main.go main.go
|
||||
COPY cmd/main.go cmd/main.go
|
||||
COPY api/ api/
|
||||
COPY controllers/ controllers/
|
||||
COPY internal/controller/ internal/controller/
|
||||
COPY pkg/ pkg/
|
||||
COPY version/ version/
|
||||
COPY vendor/ vendor/
|
||||
|
||||
# Build
|
||||
# the GOARCH has not a default value to allow the binary be built according to the host where the command
|
||||
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
|
||||
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
|
||||
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
|
||||
RUN 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} \
|
||||
go build \
|
||||
-ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \
|
||||
-mod vendor \
|
||||
-a -o manager main.go
|
||||
-o manager cmd/main.go
|
||||
|
||||
# Use distroless as minimal base image to package the manager binary
|
||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||
|
206
Makefile
206
Makefile
@@ -5,7 +5,11 @@ export MAIN_BRANCH ?= main
|
||||
# To re-generate a bundle for another specific version without changing the standard setup, you can:
|
||||
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
|
||||
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
|
||||
VERSION ?= 1.6.0
|
||||
VERSION ?= 1.9.1
|
||||
|
||||
# DEPLOYMENT_NAME defines Kubernetes deployment name for the operator.
|
||||
# It should be the same as in 'config/manager/manager.yaml'
|
||||
DEPLOYMENT_NAME ?= onepassword-connect-operator
|
||||
|
||||
# CHANNELS define the bundle channels used in the bundle.
|
||||
# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
|
||||
@@ -48,10 +52,12 @@ ifeq ($(USE_IMAGE_DIGESTS), true)
|
||||
BUNDLE_GEN_FLAGS += --use-image-digests
|
||||
endif
|
||||
|
||||
# Set the Operator SDK version to use. By default, what is installed on the system is used.
|
||||
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
|
||||
OPERATOR_SDK_VERSION ?= v1.41.1
|
||||
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= 1password/onepassword-operator:latest
|
||||
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
||||
ENVTEST_K8S_VERSION = 1.26
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
@@ -60,6 +66,12 @@ else
|
||||
GOBIN=$(shell go env GOBIN)
|
||||
endif
|
||||
|
||||
# CONTAINER_TOOL defines the container tool to be used for building images.
|
||||
# Be aware that the target commands are only tested with Docker which is
|
||||
# scaffolded by default. However, you might want to replace it to use other
|
||||
# tools. (i.e. podman)
|
||||
CONTAINER_TOOL ?= docker
|
||||
|
||||
# Setting SHELL to bash allows bash commands to be executed by recipes.
|
||||
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
|
||||
SHELL = /usr/bin/env bash -o pipefail
|
||||
@@ -72,7 +84,7 @@ all: build
|
||||
|
||||
# The help target prints out all targets with their descriptions organized
|
||||
# beneath their categories. The categories are represented by '##@' and the
|
||||
# target descriptions by '##'. The awk commands is responsible for reading the
|
||||
# target descriptions by '##'. The awk command is responsible for reading the
|
||||
# entire set of makefiles included in this invocation, looking for lines of the
|
||||
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||
@@ -104,47 +116,94 @@ vet: ## Run go vet against code.
|
||||
go vet ./...
|
||||
|
||||
.PHONY: test
|
||||
test: manifests generate fmt vet envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
|
||||
test: manifests generate fmt vet setup-envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $(shell go list ./... | grep -v /test/e2e) -coverprofile cover.out
|
||||
|
||||
# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
|
||||
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
|
||||
# CertManager is installed by default; skip with:
|
||||
# - CERT_MANAGER_INSTALL_SKIP=true
|
||||
KIND_CLUSTER ?= onepassword-operator-test-e2e
|
||||
|
||||
.PHONY: setup-test-e2e
|
||||
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
|
||||
@command -v $(KIND) >/dev/null 2>&1 || { \
|
||||
echo "Kind is not installed. Please install Kind manually."; \
|
||||
exit 1; \
|
||||
}
|
||||
@case "$$($(KIND) get clusters)" in \
|
||||
*"$(KIND_CLUSTER)"*) \
|
||||
echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
|
||||
*) \
|
||||
echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
|
||||
$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
|
||||
esac
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
|
||||
KIND_CLUSTER=$(KIND_CLUSTER) go test ./test/e2e/ -v -ginkgo.v
|
||||
$(MAKE) cleanup-test-e2e
|
||||
|
||||
.PHONY: cleanup-test-e2e
|
||||
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
|
||||
@$(KIND) delete cluster --name $(KIND_CLUSTER)
|
||||
|
||||
.PHONY: lint
|
||||
lint: golangci-lint ## Run golangci-lint linter
|
||||
$(GOLANGCI_LINT) run
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
|
||||
$(GOLANGCI_LINT) run --fix
|
||||
|
||||
.PHONY: lint-config
|
||||
lint-config: golangci-lint ## Verify golangci-lint linter configuration
|
||||
$(GOLANGCI_LINT) config verify
|
||||
|
||||
##@ Build
|
||||
|
||||
.PHONY: build
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager main.go
|
||||
go build -o bin/manager cmd/main.go
|
||||
|
||||
.PHONY: run
|
||||
run: manifests generate fmt vet ## Run a controller from your host.
|
||||
go run ./main.go
|
||||
go run ./cmd/main.go
|
||||
|
||||
# If you wish built 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.
|
||||
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
|
||||
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
|
||||
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
.PHONY: docker-build
|
||||
docker-build: test ## Build docker image with the manager.
|
||||
docker build -t ${IMG} .
|
||||
docker-build: ## Build docker image with the manager.
|
||||
DOCKER_BUILDKIT=1 $(CONTAINER_TOOL) build -t ${IMG} .
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: ## Push docker image with the manager.
|
||||
docker 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:
|
||||
# - 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/
|
||||
# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=<myregistry/image:<tag>> than the export will fail)
|
||||
# To properly provided solutions that supports more than one platform you should use this option.
|
||||
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
|
||||
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
|
||||
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
|
||||
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
|
||||
.PHONY: docker-buildx
|
||||
docker-buildx: 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
|
||||
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
|
||||
- docker buildx create --name project-v3-builder
|
||||
docker buildx use project-v3-builder
|
||||
- docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross
|
||||
- docker buildx rm project-v3-builder
|
||||
- $(CONTAINER_TOOL) buildx create --name onepassword-operator-builder
|
||||
$(CONTAINER_TOOL) buildx use onepassword-operator-builder
|
||||
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
|
||||
- $(CONTAINER_TOOL) buildx rm onepassword-operator-builder
|
||||
rm Dockerfile.cross
|
||||
|
||||
.PHONY: build-installer
|
||||
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
|
||||
mkdir -p dist
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
|
||||
$(KUSTOMIZE) build config/default > dist/install.yaml
|
||||
|
||||
##@ Deployment
|
||||
|
||||
ifndef ignore-not-found
|
||||
@@ -153,26 +212,30 @@ endif
|
||||
|
||||
.PHONY: install
|
||||
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
|
||||
$(KUSTOMIZE) build config/crd | kubectl apply -f -
|
||||
$(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f -
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
$(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
|
||||
.PHONY: set-namespace
|
||||
set-namespace:
|
||||
cd config/default && $(KUSTOMIZE) edit set namespace $(shell kubectl config view --minify -o jsonpath={..namespace})
|
||||
cd config/default && $(KUSTOMIZE) edit set namespace $(shell $(KUBECTL) config view --minify -o jsonpath={..namespace})
|
||||
|
||||
.PHONY: deploy
|
||||
deploy: manifests kustomize set-namespace ## Deploy controller to the K8s cluster specified in ~/.kube/config.
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
|
||||
$(KUSTOMIZE) build config/default | kubectl apply -f -
|
||||
$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
|
||||
|
||||
.PHONY: undeploy
|
||||
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
$(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
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 -
|
||||
|
||||
##@ 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
|
||||
LOCALBIN ?= $(shell pwd)/bin
|
||||
@@ -180,47 +243,100 @@ $(LOCALBIN):
|
||||
mkdir -p $(LOCALBIN)
|
||||
|
||||
## Tool Binaries
|
||||
KUBECTL ?= kubectl
|
||||
KIND ?= kind
|
||||
KUSTOMIZE ?= $(LOCALBIN)/kustomize
|
||||
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
|
||||
ENVTEST ?= $(LOCALBIN)/setup-envtest
|
||||
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
|
||||
|
||||
## Tool Versions
|
||||
KUSTOMIZE_VERSION ?= v4.5.7
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.10.0
|
||||
KUSTOMIZE_VERSION ?= v5.6.0
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.18.0
|
||||
# ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
|
||||
ENVTEST_VERSION := $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
|
||||
# ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
|
||||
ENVTEST_K8S_VERSION := $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
|
||||
GOLANGCI_LINT_VERSION ?= v2.2.0
|
||||
|
||||
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
|
||||
.PHONY: kustomize
|
||||
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
|
||||
$(KUSTOMIZE): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/kustomize || { curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); }
|
||||
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
|
||||
|
||||
.PHONY: controller-gen
|
||||
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
|
||||
$(CONTROLLER_GEN): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/controller-gen || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
|
||||
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
|
||||
|
||||
.PHONY: setup-envtest
|
||||
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
|
||||
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
|
||||
@$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \
|
||||
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
.PHONY: envtest
|
||||
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
|
||||
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
|
||||
$(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
|
||||
OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk
|
||||
operator-sdk: ## Download operator-sdk locally if necessary.
|
||||
ifeq (,$(wildcard $(OPERATOR_SDK)))
|
||||
ifeq (, $(shell which operator-sdk 2>/dev/null))
|
||||
@{ \
|
||||
set -e ;\
|
||||
mkdir -p $(dir $(OPERATOR_SDK)) ;\
|
||||
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
|
||||
curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\
|
||||
chmod +x $(OPERATOR_SDK) ;\
|
||||
}
|
||||
else
|
||||
OPERATOR_SDK = $(shell which operator-sdk)
|
||||
endif
|
||||
endif
|
||||
|
||||
.PHONY: bundle
|
||||
bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files.
|
||||
operator-sdk generate kustomize manifests -q
|
||||
bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files.
|
||||
$(OPERATOR_SDK) generate kustomize manifests -q
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG)
|
||||
$(KUSTOMIZE) build config/manifests | operator-sdk generate bundle $(BUNDLE_GEN_FLAGS)
|
||||
operator-sdk bundle validate ./bundle
|
||||
$(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS)
|
||||
$(OPERATOR_SDK) bundle validate ./bundle
|
||||
|
||||
.PHONY: bundle-build
|
||||
bundle-build: ## Build the bundle image.
|
||||
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
|
||||
$(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
|
||||
|
||||
.PHONY: bundle-push
|
||||
bundle-push: ## Push the bundle image.
|
||||
$(MAKE) docker-push IMG=$(BUNDLE_IMG)
|
||||
|
||||
.PHONY: opm
|
||||
OPM = ./bin/opm
|
||||
OPM = $(LOCALBIN)/opm
|
||||
opm: ## Download opm locally if necessary.
|
||||
ifeq (,$(wildcard $(OPM)))
|
||||
ifeq (,$(shell which opm 2>/dev/null))
|
||||
@@ -228,7 +344,7 @@ ifeq (,$(shell which opm 2>/dev/null))
|
||||
set -e ;\
|
||||
mkdir -p $(dir $(OPM)) ;\
|
||||
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
|
||||
curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.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) ;\
|
||||
}
|
||||
else
|
||||
@@ -253,7 +369,7 @@ endif
|
||||
# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
|
||||
.PHONY: catalog-build
|
||||
catalog-build: opm ## Build a catalog image.
|
||||
$(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
|
||||
$(OPM) index add --container-tool $(CONTAINER_TOOL) --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
|
||||
|
||||
# Push the catalog image.
|
||||
.PHONY: catalog-push
|
||||
|
6
PROJECT
6
PROJECT
@@ -1,6 +1,10 @@
|
||||
# Code generated by tool. DO NOT EDIT.
|
||||
# This file is used to track the info used to scaffold your project
|
||||
# and allow the plugins properly work.
|
||||
# More info: https://book.kubebuilder.io/reference/project-config.html
|
||||
domain: onepassword.com
|
||||
layout:
|
||||
- go.kubebuilder.io/v4-alpha
|
||||
- go.kubebuilder.io/v4
|
||||
plugins:
|
||||
manifests.sdk.operatorframework.io/v2: {}
|
||||
scorecard.sdk.operatorframework.io/v2: {}
|
||||
|
15
README.md
15
README.md
@@ -17,25 +17,31 @@ The 1Password Connect Kubernetes Operator provides the ability to integrate Kube
|
||||
|
||||
### 🚀 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:
|
||||
|
||||
```
|
||||
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:
|
||||
```apiVersion: onepassword.com/v1
|
||||
|
||||
```
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: <item_name> #this name will also be used for naming the generated kubernetes secret
|
||||
spec:
|
||||
itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
|
||||
```
|
||||
|
||||
Deploy the OnePasswordItem to Kubernetes:
|
||||
|
||||
```
|
||||
kubectl apply -f <your_item>.yaml
|
||||
```
|
||||
|
||||
Check that the Kubernetes Secret has been generated:
|
||||
|
||||
```
|
||||
@@ -43,6 +49,7 @@ kubectl get secret <secret_name>
|
||||
```
|
||||
|
||||
### 📄 Usage
|
||||
|
||||
Refer to the [Usage Guide](USAGEGUIDE.md) for documentation on how to deploy and use the 1Password Operator.
|
||||
|
||||
## 💙 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.
|
||||
|
||||
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
|
||||
|
||||
For information about security practices, please visit the [1Password Bug Bounty Program](https://bugcrowd.com/agilebits).
|
||||
Please file requests by sending an email to bugbounty@agilebits.com.
|
||||
|
186
USAGEGUIDE.md
186
USAGEGUIDE.md
@@ -5,107 +5,53 @@
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Deploying 1Password Connect to Kubernetes](#deploying-1password-connect-to-kubernetes)
|
||||
- [Kubernetes Operator Deployment](#kubernetes-operator-deployment)
|
||||
- [Usage](#usage)
|
||||
- [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
|
||||
- [Development](#development)
|
||||
1. [Configuration Options](#configuration-options)
|
||||
2. [Use Kubernetes Operator with Service Account](#use-kubernetes-operator-with-service-account)
|
||||
- [Create a Service Account](#1-create-a-service-account)
|
||||
- [Create a Kubernetes secret](#2-create-a-kubernetes-secret-for-the-service-account)
|
||||
- [Deploy the Operator](#3-deploy-the-operator)
|
||||
3. [Use Kubernetes Operator with Connect](#use-kubernetes-operator-with-connect)
|
||||
- [Deploy with Helm](#1-deploy-with-helm)
|
||||
- [Deploy manually](#2-deploy-manually)
|
||||
4. [Logging level](#logging-level)
|
||||
5. [Usage examples](#usage-examples)
|
||||
6. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets)
|
||||
7. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
|
||||
8. [Development](#development)
|
||||
|
||||
## 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)
|
||||
- [Deploy using the Connect Operator](#deploy-using-the-connect-operator)
|
||||
|
||||
### Deploy with Helm
|
||||
|
||||
The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes.
|
||||
|
||||
[The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts)
|
||||
|
||||
### Deploy using the Connect Operator
|
||||
|
||||
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`:
|
||||
### 1. [Create a service account](https://developer.1password.com/docs/service-accounts/get-started#create-a-service-account)
|
||||
### 2. Create a Kubernetes secret for the Service Account
|
||||
- Set `OP_SERVICE_ACCOUNT_TOKEN` environment variable to the service account token you created in the previous step. This token will be used by the operator to access 1Password items.
|
||||
- Create Kubernetes secret:
|
||||
|
||||
```bash
|
||||
cat 1password-credentials.json | base64 | \
|
||||
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
|
||||
kubectl create secret generic onepassword-service-account-token --from-literal=token="$OP_SERVICE_ACCOUNT_TOKEN"
|
||||
```
|
||||
|
||||
Create a Kubernetes secret from the op-session file:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic op-credentials --from-file=op-session
|
||||
```
|
||||
|
||||
Add the following environment variable to the onepassword-connect-operator container in `/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**
|
||||
### 3. Deploy the Operator
|
||||
|
||||
An sample Deployment yaml can be found at `/config/manager/manager.yaml`.
|
||||
To use Operator with Service Account, you need to set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable in the `/config/manager/manager.yaml`. And remove `OP_CONNECT_TOKEN` and `OP_CONNECT_HOST` environment variables.
|
||||
|
||||
To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml:
|
||||
|
||||
- **OP_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.
|
||||
- **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.
|
||||
- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password.
|
||||
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
|
||||
|
||||
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:
|
||||
|
||||
```shell
|
||||
@@ -118,59 +64,57 @@ make deploy
|
||||
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
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: <item_name> #this name will also be used for naming the generated kubernetes secret
|
||||
spec:
|
||||
itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
|
||||
....
|
||||
containers:
|
||||
- command:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
- --zap-log-level=info
|
||||
image: 1password/onepassword-operator:latest
|
||||
....
|
||||
```
|
||||
|
||||
Deploy the OnePasswordItem to Kubernetes:
|
||||
---
|
||||
|
||||
```bash
|
||||
kubectl apply -f <your_item>.yaml
|
||||
```
|
||||
## Usage examples
|
||||
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
|
||||
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.
|
||||
## How 1Password Items Map to Kubernetes Secrets
|
||||
|
||||
The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password.
|
||||
In case of fields that store files, the file's contents will be used as the value.
|
||||
|
||||
Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
|
||||
@@ -237,6 +181,8 @@ metadata:
|
||||
|
||||
If the value is not set, the auto restart settings on the deployment will be used.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### How it works
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -67,8 +67,8 @@ type OnePasswordItemStatus struct {
|
||||
Conditions []OnePasswordItemCondition `json:"conditions"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
//+kubebuilder:subresource:status
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
|
||||
// OnePasswordItem is the Schema for the onepassworditems API
|
||||
type OnePasswordItem struct {
|
||||
@@ -81,7 +81,7 @@ type OnePasswordItem struct {
|
||||
Status OnePasswordItemStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// OnePasswordItemList contains a list of OnePasswordItem
|
||||
type OnePasswordItemList struct {
|
||||
|
@@ -1,10 +1,9 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
428
cmd/main.go
Normal file
428
cmd/main.go
Normal file
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
k8sruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
"github.com/1Password/onepassword-operator/internal/controller"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
"github.com/1Password/onepassword-operator/version"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = k8sruntime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
const (
|
||||
envPollingIntervalVariable = "POLLING_INTERVAL"
|
||||
manageConnect = "MANAGE_CONNECT"
|
||||
restartDeploymentsEnvVariable = "AUTO_RESTART"
|
||||
defaultPollingInterval = 600
|
||||
|
||||
annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
|
||||
)
|
||||
|
||||
func printVersion() {
|
||||
setupLog.Info(fmt.Sprintf("Operator Version: %s", version.OperatorVersion))
|
||||
setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
|
||||
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
|
||||
setupLog.Info(fmt.Sprintf("Version of operator-sdk: %v", version.OperatorSDKVersion))
|
||||
}
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(onepasswordcomv1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var metricsCertPath, metricsCertName, metricsCertKey string
|
||||
var webhookCertPath, webhookCertName, webhookCertKey string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "8080",
|
||||
"The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081",
|
||||
"The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
|
||||
"The directory that contains the metrics server certificate.")
|
||||
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt",
|
||||
"The name of the metrics server certificate file.")
|
||||
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key",
|
||||
"The name of the metrics server key file.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics")
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancelation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
printVersion()
|
||||
|
||||
// Create a root context that will be cancelled on termination signals
|
||||
ctx := ctrl.SetupSignalHandler()
|
||||
|
||||
watchNamespace, err := getWatchNamespace()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to get WatchNamespace, "+
|
||||
"the manager will watch and manage resources in all namespaces")
|
||||
}
|
||||
|
||||
deploymentNamespace, err := utils.GetOperatorNamespace()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "Failed to get namespace")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create watchers for metrics and webhooks certificates
|
||||
var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher
|
||||
|
||||
// Initial webhook TLS options
|
||||
webhookTLSOpts := tlsOpts
|
||||
|
||||
if len(webhookCertPath) > 0 {
|
||||
setupLog.Info("Initializing webhook certificate watcher using provided certificates",
|
||||
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
|
||||
|
||||
var err error
|
||||
webhookCertWatcher, err = certwatcher.New(
|
||||
filepath.Join(webhookCertPath, webhookCertName),
|
||||
filepath.Join(webhookCertPath, webhookCertKey),
|
||||
)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "Failed to initialize webhook certificate watcher")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) {
|
||||
config.GetCertificate = webhookCertWatcher.GetCertificate
|
||||
})
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: webhookTLSOpts,
|
||||
})
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
// TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are
|
||||
// not provided, self-signed certificates will be generated by default. This option is not recommended for
|
||||
// production environments as self-signed certificates do not offer the same level of trust and security
|
||||
// as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing
|
||||
// unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName
|
||||
// to provide certificates, ensuring the server communicates using trusted and secure certificates.
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
}
|
||||
|
||||
// If the certificate is not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
//
|
||||
// TODO(user): If you enable certManager, uncomment the following lines:
|
||||
// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
|
||||
// managed by cert-manager for the metrics server.
|
||||
// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
|
||||
if len(metricsCertPath) > 0 {
|
||||
setupLog.Info("Initializing metrics certificate watcher using provided certificates",
|
||||
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
|
||||
|
||||
var err error
|
||||
metricsCertWatcher, err = certwatcher.New(
|
||||
filepath.Join(metricsCertPath, metricsCertName),
|
||||
filepath.Join(metricsCertPath, metricsCertKey),
|
||||
)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "Failed to initialize metrics certificate watcher")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) {
|
||||
config.GetCertificate = metricsCertWatcher.GetCertificate
|
||||
})
|
||||
}
|
||||
|
||||
options := ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "c26807fd.onepassword.com",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
}
|
||||
|
||||
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
|
||||
if watchNamespace != "" {
|
||||
namespaces := strings.Split(watchNamespace, ",")
|
||||
namespaceMap := make(map[string]cache.Config)
|
||||
for _, namespace := range namespaces {
|
||||
namespaceMap[namespace] = cache.Config{}
|
||||
}
|
||||
options.NewCache = func(config *rest.Config, opts cache.Options) (cache.Cache, error) {
|
||||
opts.DefaultNamespaces = namespaceMap
|
||||
return cache.New(config, opts)
|
||||
}
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup One Password Client
|
||||
opClient, err := opclient.NewFromEnvironment(ctx, opclient.Config{
|
||||
Logger: setupLog,
|
||||
Version: version.OperatorVersion,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to create 1Password client")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.OnePasswordItemReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
OpClient: opClient,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
r, _ := regexp.Compile(annotationRegExpString)
|
||||
if err = (&controller.DeploymentReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
OpClient: opClient,
|
||||
OpAnnotationRegExp: r,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Deployment")
|
||||
os.Exit(1)
|
||||
}
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
// Setup 1PasswordConnect
|
||||
if shouldManageConnect() {
|
||||
setupLog.Info("Automated Connect Management Enabled")
|
||||
go func(ctx context.Context) {
|
||||
connectStarted := false
|
||||
for !connectStarted {
|
||||
err := op.SetupConnect(ctx, mgr.GetClient(), deploymentNamespace)
|
||||
// Cache Not Started is an acceptable error. Retry until cache is started.
|
||||
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err == nil {
|
||||
connectStarted = true
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
} else {
|
||||
setupLog.Info("Automated Connect Management Disabled")
|
||||
}
|
||||
|
||||
// Setup update secrets task
|
||||
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opClient, shouldAutoRestartDeployments())
|
||||
done := make(chan bool)
|
||||
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := updatedSecretsPoller.UpdateKubernetesSecretsTask(ctx)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "error running update kubernetes secret task")
|
||||
}
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
if metricsCertWatcher != nil {
|
||||
setupLog.Info("Adding metrics certificate watcher to manager")
|
||||
if err := mgr.Add(metricsCertWatcher); err != nil {
|
||||
setupLog.Error(err, "Unable to add metrics certificate watcher to manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctx); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// getWatchNamespace returns the Namespace the operator should be watching for changes
|
||||
func getWatchNamespace() (string, error) {
|
||||
// WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE
|
||||
// which specifies the Namespace to watch.
|
||||
// An empty value means the operator is running with cluster scope.
|
||||
var watchNamespaceEnvVar = "WATCH_NAMESPACE"
|
||||
|
||||
ns, found := os.LookupEnv(watchNamespaceEnvVar)
|
||||
if !found {
|
||||
return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar)
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func shouldManageConnect() bool {
|
||||
shouldManageConnect, found := os.LookupEnv(manageConnect)
|
||||
if found {
|
||||
shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldManageConnectBool
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldAutoRestartDeployments() bool {
|
||||
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
|
||||
if found {
|
||||
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldAutoRestartDeploymentsBool
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getPollingIntervalForUpdatingSecrets() time.Duration {
|
||||
timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable)
|
||||
if found {
|
||||
timeInSeconds, err := strconv.Atoi(timeInSecondsString)
|
||||
if err == nil {
|
||||
return time.Duration(timeInSeconds) * time.Second
|
||||
}
|
||||
setupLog.Info("Invalid value set for polling interval. Must be a valid integer.")
|
||||
}
|
||||
|
||||
setupLog.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval))
|
||||
return time.Duration(defaultPollingInterval) * time.Second
|
||||
}
|
@@ -14,6 +14,8 @@ spec:
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
fsGroup: 999
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
volumes:
|
||||
- name: shared-data
|
||||
emptyDir: {}
|
||||
@@ -31,14 +33,24 @@ spec:
|
||||
volumeMounts:
|
||||
- mountPath: /home/opuser/.op/data
|
||||
name: shared-data
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsNonRoot: false
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ "ALL" ]
|
||||
containers:
|
||||
- name: connect-api
|
||||
image: 1password/connect-api:latest
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
requests:
|
||||
cpu: "0.2"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -54,10 +66,14 @@ spec:
|
||||
- name: connect-sync
|
||||
image: 1password/connect-sync:latest
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
requests:
|
||||
cpu: "0.2"
|
||||
ports:
|
||||
- containerPort: 8081
|
||||
|
@@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
creationTimestamp: null
|
||||
controller-gen.kubebuilder.io/version: v0.18.0
|
||||
name: onepassworditems.onepassword.com
|
||||
spec:
|
||||
group: onepassword.com
|
||||
@@ -21,14 +20,19 @@ spec:
|
||||
description: OnePasswordItem is the Schema for the onepassworditems API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
|
@@ -5,17 +5,13 @@ resources:
|
||||
- bases/onepassword.com_onepassworditems.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizeresource
|
||||
|
||||
patchesStrategicMerge:
|
||||
patches:
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
|
||||
# patches here are for enabling the conversion webhook for each CRD
|
||||
#- patches/webhook_in_onepassworditems.yaml
|
||||
#- path: patches/webhook_in_onepassworditems.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizewebhookpatch
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
|
||||
# patches here are for enabling the CA injection for each CRD
|
||||
#- patches/cainjection_in_onepassworditems.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
|
||||
|
||||
# [WEBHOOK] To enable webhook, uncomment the following section
|
||||
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||
configurations:
|
||||
- kustomizeconfig.yaml
|
||||
#configurations:
|
||||
#- kustomizeconfig.yaml
|
||||
|
@@ -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
|
@@ -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
|
@@ -1,3 +1,6 @@
|
||||
# Adds namespace to all resources.
|
||||
# namespace: onepassword-connect-operator
|
||||
|
||||
# Value of this field is prepended to the
|
||||
# names of all resources, e.g. a deployment named
|
||||
# "wordpress" becomes "alices-wordpress".
|
||||
@@ -22,122 +25,210 @@ resources:
|
||||
#- ../certmanager
|
||||
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||
#- ../prometheus
|
||||
# [METRICS] Expose the controller manager metrics service.
|
||||
- metrics_service.yaml
|
||||
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
|
||||
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
|
||||
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
|
||||
# be able to communicate with the Webhook Server.
|
||||
#- ../network-policy
|
||||
|
||||
patchesStrategicMerge:
|
||||
# Protect the /metrics endpoint by putting it behind auth.
|
||||
# If you want your controller-manager to expose the /metrics
|
||||
# endpoint w/o any authn/z, please comment the following line.
|
||||
- manager_auth_proxy_patch.yaml
|
||||
# Uncomment the patches line if you enable Metrics
|
||||
patches:
|
||||
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
|
||||
# More info: https://book.kubebuilder.io/reference/metrics
|
||||
- path: manager_metrics_patch.yaml
|
||||
target:
|
||||
kind: Deployment
|
||||
|
||||
# Mount the controller config file for loading manager configurations
|
||||
# through a ComponentConfig type
|
||||
#- manager_config_patch.yaml
|
||||
# Uncomment the patches line if you enable Metrics and CertManager
|
||||
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
|
||||
# This patch will protect the metrics with certManager self-signed certs.
|
||||
#- path: cert_metrics_manager_patch.yaml
|
||||
# target:
|
||||
# kind: Deployment
|
||||
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- manager_webhook_patch.yaml
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
|
||||
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
|
||||
# 'CERTMANAGER' needs to be enabled to use ca injection
|
||||
#- webhookcainjection_patch.yaml
|
||||
#- path: manager_webhook_patch.yaml
|
||||
# target:
|
||||
# kind: Deployment
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
||||
# Uncomment the following replacements to add the cert-manager CA injection annotations
|
||||
#replacements:
|
||||
# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # this name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.namespace # namespace of the certificate CR
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - 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:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # this name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
# - select:
|
||||
# kind: CustomResourceDefinition
|
||||
# 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
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# 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
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
||||
# - source: # Uncomment the following block to enable certificates for metrics
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: controller-manager-metrics-service
|
||||
# fieldPath: metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: metrics-certs
|
||||
# fieldPaths:
|
||||
# - spec.dnsNames.0
|
||||
# - spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
|
||||
# kind: ServiceMonitor
|
||||
# group: monitoring.coreos.com
|
||||
# version: v1
|
||||
# name: controller-manager-metrics-monitor
|
||||
# fieldPaths:
|
||||
# - spec.endpoints.0.tlsConfig.serverName
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 0
|
||||
# create: true
|
||||
#
|
||||
# - source:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: controller-manager-metrics-service
|
||||
# fieldPath: metadata.namespace
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: metrics-certs
|
||||
# fieldPaths:
|
||||
# - spec.dnsNames.0
|
||||
# - spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
||||
# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
|
||||
# kind: ServiceMonitor
|
||||
# group: monitoring.coreos.com
|
||||
# version: v1
|
||||
# name: controller-manager-metrics-monitor
|
||||
# fieldPaths:
|
||||
# - spec.endpoints.0.tlsConfig.serverName
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
||||
#
|
||||
# - source: # Uncomment the following block if you have any webhook
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldPath: .metadata.name # Name of the service
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldPath: .metadata.namespace # Namespace of the service
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
||||
#
|
||||
# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # This name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
#
|
||||
# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
#
|
||||
# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.namespace # Namespace of the certificate CR
|
||||
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
|
||||
# +kubebuilder:scaffold:crdkustomizecainjectionns
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert
|
||||
# fieldPath: .metadata.name
|
||||
# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
|
||||
# +kubebuilder:scaffold:crdkustomizecainjectionname
|
||||
|
@@ -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.13.1
|
||||
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"
|
@@ -1,22 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
containers:
|
||||
- name: manager
|
||||
args:
|
||||
- "--config=controller_manager_config.yaml"
|
||||
volumeMounts:
|
||||
- name: manager-config
|
||||
mountPath: /controller_manager_config.yaml
|
||||
subPath: controller_manager_config.yaml
|
||||
volumes:
|
||||
- name: manager-config
|
||||
configMap:
|
||||
name: manager-config
|
4
config/default/manager_metrics_patch.yaml
Normal file
4
config/default/manager_metrics_patch.yaml
Normal 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
|
17
config/default/metrics_service.yaml
Normal file
17
config/default/metrics_service.yaml
Normal 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
|
@@ -1,21 +0,0 @@
|
||||
apiVersion: controller-runtime.sigs.k8s.io/v1alpha1
|
||||
kind: ControllerManagerConfig
|
||||
health:
|
||||
healthProbeBindAddress: :8081
|
||||
metrics:
|
||||
bindAddress: 127.0.0.1:8080
|
||||
webhook:
|
||||
port: 9443
|
||||
leaderElection:
|
||||
leaderElect: true
|
||||
resourceName: c26807fd.onepassword.com
|
||||
# leaderElectionReleaseOnCancel defines if the leader should step down volume
|
||||
# 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
|
@@ -1,10 +1,8 @@
|
||||
resources:
|
||||
- manager.yaml
|
||||
|
||||
generatorOptions:
|
||||
disableNameSuffixHash: true
|
||||
|
||||
configMapGenerator:
|
||||
- name: manager-config
|
||||
files:
|
||||
- controller_manager_config.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
images:
|
||||
- name: controller
|
||||
newName: 1password/onepassword-operator
|
||||
newTag: latest
|
||||
|
@@ -1,14 +1,34 @@
|
||||
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:
|
||||
name: onepassword-connect-operator
|
||||
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:
|
||||
@@ -16,7 +36,28 @@ spec:
|
||||
kubectl.kubernetes.io/default-container: manager
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
spec:
|
||||
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
|
||||
# according to the platforms which are supported by your solution.
|
||||
# It is considered best practice to support multiple architectures. You can
|
||||
# build your manager image using the makefile target docker-buildx.
|
||||
# affinity:
|
||||
# nodeAffinity:
|
||||
# requiredDuringSchedulingIgnoredDuringExecution:
|
||||
# nodeSelectorTerms:
|
||||
# - matchExpressions:
|
||||
# - key: kubernetes.io/arch
|
||||
# operator: In
|
||||
# values:
|
||||
# - amd64
|
||||
# - arm64
|
||||
# - ppc64le
|
||||
# - s390x
|
||||
# - key: kubernetes.io/os
|
||||
# operator: In
|
||||
# values:
|
||||
# - linux
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
# TODO(user): For common cases that do not require escalating privileges
|
||||
@@ -31,33 +72,43 @@ spec:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
- --health-probe-bind-address=:8081
|
||||
image: 1password/onepassword-operator:latest
|
||||
imagePullPolicy: Never
|
||||
name: manager
|
||||
env:
|
||||
- name: WATCH_NAMESPACE
|
||||
value: "default"
|
||||
- name: OPERATOR_NAME
|
||||
value: "onepassword-connect-operator"
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: OPERATOR_NAME
|
||||
value: "onepassword-connect-operator"
|
||||
- name: OP_CONNECT_HOST
|
||||
value: "http://onepassword-connect:8080"
|
||||
- name: WATCH_NAMESPACE
|
||||
value: "default"
|
||||
- name: POLLING_INTERVAL
|
||||
value: "10"
|
||||
- name: AUTO_RESTART
|
||||
value: "false"
|
||||
- name: OP_CONNECT_HOST
|
||||
value: "http://onepassword-connect:8080"
|
||||
- name: OP_CONNECT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: onepassword-token
|
||||
key: token
|
||||
- name: AUTO_RESTART
|
||||
value: "false"
|
||||
- name: MANAGE_CONNECT
|
||||
value: "true"
|
||||
# Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN, OP_CONNECT_HOST and MANAGE_CONNECT env vars.
|
||||
# - name: OP_SERVICE_ACCOUNT_TOKEN
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: onepassword-service-account-token
|
||||
# key: token
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
- "ALL"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
@@ -75,9 +126,9 @@ spec:
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
serviceAccountName: onepassword-connect-operator
|
||||
terminationGracePeriodSeconds: 10
|
||||
|
26
config/network-policy/allow-metrics-traffic.yaml
Normal file
26
config/network-policy/allow-metrics-traffic.yaml
Normal 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
|
2
config/network-policy/kustomization.yaml
Normal file
2
config/network-policy/kustomization.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- allow-metrics-traffic.yaml
|
@@ -1,2 +1,11 @@
|
||||
resources:
|
||||
- monitor.yaml
|
||||
|
||||
# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
|
||||
# to securely reference certificates created and managed by cert-manager.
|
||||
# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
|
||||
# to mount the "metrics-server-cert" secret in the Manager Deployment.
|
||||
#patches:
|
||||
# - path: monitor_tls_patch.yaml
|
||||
# target:
|
||||
# kind: ServiceMonitor
|
||||
|
@@ -1,20 +1,37 @@
|
||||
|
||||
# Prometheus Monitor Service (Metrics)
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
app.kubernetes.io/name: onepassword-operator
|
||||
app.kubernetes.io/instance: controller-manager-metrics-monitor
|
||||
app.kubernetes.io/component: metrics
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: onepassword-connect-operator-metrics-monitor
|
||||
namespace: system
|
||||
spec:
|
||||
endpoints:
|
||||
- path: /metrics
|
||||
port: https
|
||||
port: https # Ensure this is the name of the port that exposes HTTPS metrics
|
||||
scheme: https
|
||||
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
tlsConfig:
|
||||
# TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
|
||||
# certificate verification. This poses a significant security risk by making the system vulnerable to
|
||||
# man-in-the-middle attacks, where an attacker could intercept and manipulate the communication between
|
||||
# Prometheus and the monitored services. This could lead to unauthorized access to sensitive metrics data,
|
||||
# compromising the integrity and confidentiality of the information.
|
||||
# Please use the following options for secure configurations:
|
||||
# caFile: /etc/metrics-certs/ca.crt
|
||||
# certFile: /etc/metrics-certs/tls.crt
|
||||
# keyFile: /etc/metrics-certs/tls.key
|
||||
insecureSkipVerify: true
|
||||
selector:
|
||||
matchLabels:
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
app.kubernetes.io/name: onepassword-operator
|
||||
|
19
config/prometheus/monitor_tls_patch.yaml
Normal file
19
config/prometheus/monitor_tls_patch.yaml
Normal 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
|
@@ -1,17 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: proxy-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
name: onepassword-connect-operator-metrics-service
|
||||
namespace: system
|
||||
spec:
|
||||
ports:
|
||||
- name: https
|
||||
port: 8443
|
||||
protocol: TCP
|
||||
targetPort: https
|
||||
selector:
|
||||
name: onepassword-connect-operator
|
@@ -9,10 +9,19 @@ resources:
|
||||
- role_binding.yaml
|
||||
- leader_election_role.yaml
|
||||
- leader_election_role_binding.yaml
|
||||
# Comment the following 4 lines if you want to disable
|
||||
# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
|
||||
# which protects your /metrics endpoint.
|
||||
- auth_proxy_service.yaml
|
||||
- auth_proxy_role.yaml
|
||||
- auth_proxy_role_binding.yaml
|
||||
- auth_proxy_client_clusterrole.yaml
|
||||
# The following RBAC configurations are used to protect
|
||||
# the metrics endpoint with authn/authz. These configurations
|
||||
# ensure that only authorized users and service accounts
|
||||
# can access the metrics endpoint. Comment the following
|
||||
# permissions if you want to disable this protection.
|
||||
# More info: https://book.kubebuilder.io/reference/metrics.html
|
||||
- metrics_auth_role.yaml
|
||||
- metrics_auth_role_binding.yaml
|
||||
- metrics_reader_role.yaml
|
||||
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
|
||||
# default, aiding admins in cluster management. Those roles are
|
||||
# not used by the {{ .ProjectName }} itself. You can comment the following lines
|
||||
# if you do not want those helpers be installed with your Project.
|
||||
- onepassworditem_admin_role.yaml
|
||||
- onepassworditem_editor_role.yaml
|
||||
- onepassworditem_viewer_role.yaml
|
||||
|
@@ -2,6 +2,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: role
|
||||
app.kubernetes.io/instance: leader-election-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: leader-election-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
|
@@ -1,6 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: rolebinding
|
||||
app.kubernetes.io/instance: leader-election-rolebinding
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: leader-election-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
17
config/rbac/metrics_auth_role.yaml
Normal file
17
config/rbac/metrics_auth_role.yaml
Normal 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
|
@@ -1,12 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: proxy-rolebinding
|
||||
name: metrics-auth-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: proxy-role
|
||||
name: metrics-auth-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
namespace: system
|
@@ -3,7 +3,7 @@ kind: ClusterRole
|
||||
metadata:
|
||||
name: metrics-reader
|
||||
rules:
|
||||
- nonResourceURLs:
|
||||
- "/metrics"
|
||||
verbs:
|
||||
- get
|
||||
- nonResourceURLs:
|
||||
- "/metrics"
|
||||
verbs:
|
||||
- get
|
31
config/rbac/onepassworditem_admin_role.yaml
Normal file
31
config/rbac/onepassworditem_admin_role.yaml
Normal 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
|
@@ -1,7 +1,20 @@
|
||||
# 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
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: onepassworditem-editor-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: onepassworditem-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
|
@@ -1,7 +1,20 @@
|
||||
# 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
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: onepassworditem-viewer-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: onepassworditem-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
|
@@ -2,7 +2,6 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
@@ -25,12 +24,6 @@ rules:
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
@@ -46,25 +39,6 @@ rules:
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
- replicasets
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
@@ -87,6 +61,15 @@ rules:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- update
|
||||
- apiGroups:
|
||||
- monitoring.coreos.com
|
||||
resources:
|
||||
@@ -98,17 +81,6 @@ rules:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems
|
||||
verbs:
|
||||
- create
|
||||
|
@@ -1,6 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrolebinding
|
||||
app.kubernetes.io/instance: manager-rolebinding
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: manager-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
@@ -1,5 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: serviceaccount
|
||||
app.kubernetes.io/instance: controller-manager-sa
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
|
@@ -1,6 +1,12 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: onepassworditem
|
||||
app.kubernetes.io/instance: onepassworditem-sample
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
name: onepassworditem-sample
|
||||
spec:
|
||||
itemPath: "vaults/<vault_id>/items/<item_id>"
|
||||
|
@@ -4,7 +4,7 @@
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- basic-check-spec
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
image: quay.io/operator-framework/scorecard-test:v1.33.0
|
||||
labels:
|
||||
suite: basic
|
||||
test: basic-check-spec-test
|
||||
|
@@ -4,7 +4,7 @@
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-bundle-validation
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
image: quay.io/operator-framework/scorecard-test:v1.33.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-bundle-validation-test
|
||||
@@ -14,7 +14,7 @@
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-crds-have-validation
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
image: quay.io/operator-framework/scorecard-test:v1.33.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-crds-have-validation-test
|
||||
@@ -24,7 +24,7 @@
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-crds-have-resources
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
image: quay.io/operator-framework/scorecard-test:v1.33.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-crds-have-resources-test
|
||||
@@ -34,7 +34,7 @@
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-spec-descriptors
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
image: quay.io/operator-framework/scorecard-test:v1.33.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-spec-descriptors-test
|
||||
@@ -44,7 +44,7 @@
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-status-descriptors
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
image: quay.io/operator-framework/scorecard-test:v1.33.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-status-descriptors-test
|
||||
|
27
docs/testing.md
Normal file
27
docs/testing.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Testing
|
||||
|
||||
## Unit tests
|
||||
**When**: Pure Go logic, no Kubernetes apiserver or network.
|
||||
**Where**: `internal/...`, `pkg/...`
|
||||
**Add files in**: `*_test.go` next to the code.
|
||||
**Run**: `make test`
|
||||
|
||||
## Integration tests (envtest)
|
||||
**When**: Controller/reconciler behavior against a mocked kubernetes cluster.
|
||||
**Where**: `internal/controller/...`
|
||||
**Framework**: controller-runtime’s `envtest`.
|
||||
**Run**: `make test`
|
||||
|
||||
## E2E tests (kind)
|
||||
**When**: Full cluster behavior (CRDs, operator image, Connect/SA flows).
|
||||
**Where**: `test/e2e/...`
|
||||
**Framework**: Ginkgo + `pkg/testhelper`.
|
||||
|
||||
**Local prep**:
|
||||
1. [Install `kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installing-with-a-package-manager) to spin up local Kubernetes cluster.
|
||||
2. `export OP_CONNECT_TOKEN=<token>`
|
||||
3. `export OP_SERVICE_ACCOUNT_TOKEN=<token>`
|
||||
4. `make test-e2e`
|
||||
5. Put `1password-credentials.json` into project root.
|
||||
|
||||
**Run**: `make test-e2e`
|
149
go.mod
149
go.mod
@@ -1,81 +1,116 @@
|
||||
module github.com/1Password/onepassword-operator
|
||||
|
||||
go 1.20
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.5
|
||||
|
||||
require (
|
||||
github.com/1Password/connect-sdk-go v1.5.1
|
||||
github.com/onsi/ginkgo/v2 v2.9.2
|
||||
github.com/onsi/gomega v1.27.5
|
||||
github.com/stretchr/testify v1.8.2
|
||||
k8s.io/api v0.26.3
|
||||
k8s.io/apimachinery v0.26.3
|
||||
k8s.io/client-go v0.26.3
|
||||
k8s.io/kubectl v0.26.3
|
||||
sigs.k8s.io/controller-runtime v0.14.5
|
||||
github.com/1Password/connect-sdk-go v1.5.3
|
||||
github.com/1password/onepassword-sdk-go v0.3.1
|
||||
github.com/go-logr/logr v1.4.2
|
||||
github.com/onsi/ginkgo/v2 v2.22.0
|
||||
github.com/onsi/gomega v1.36.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
k8s.io/api v0.33.0
|
||||
k8s.io/apiextensions-apiserver v0.33.0
|
||||
k8s.io/apimachinery v0.33.0
|
||||
k8s.io/client-go v0.33.0
|
||||
k8s.io/kubectl v0.29.0
|
||||
sigs.k8s.io/controller-runtime v0.21.0
|
||||
)
|
||||
|
||||
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/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/zapr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/extism/go-sdk v1.7.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.23.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/cobra v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.30.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/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/grpc v1.68.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.26.3 // indirect
|
||||
k8s.io/component-base v0.26.3 // indirect
|
||||
k8s.io/klog/v2 v2.90.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect
|
||||
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
k8s.io/apiserver v0.33.0 // indirect
|
||||
k8s.io/component-base v0.33.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
448
go.sum
448
go.sum
@@ -1,123 +1,101 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/1Password/connect-sdk-go v1.5.1 h1:wb9niRg4BOa+lZJjj1TOX6093VJxuOYtzqUnRpwKnvs=
|
||||
github.com/1Password/connect-sdk-go v1.5.1/go.mod h1:lKGz6DFO6qMchEQ+lDx6f9MzORTxC1HkhUdHnJ24fKs=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
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/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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
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/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
|
||||
github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
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/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/extism/go-sdk v1.7.0 h1:yHbSa2JbcF60kjGsYiGEOcClfbknqCJchyh9TRibFWo=
|
||||
github.com/extism/go-sdk v1.7.0/go.mod h1:Dhuc1qcD0aqjdqJ3ZDyGdkZPEj/EHKVjbE4P+1XRMqc=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
|
||||
github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
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/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
|
||||
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4=
|
||||
github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo=
|
||||
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
|
||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
|
||||
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/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/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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/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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -125,214 +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/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/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||
github.com/onsi/gomega v1.27.5 h1:T/X6I0RNFw/kTqgfkZPcQ5KU6vCnWNBGdtrIx2dpGeQ=
|
||||
github.com/onsi/gomega v1.27.5/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
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/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/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/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
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.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.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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
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.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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
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/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/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||
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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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-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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
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.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||
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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/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/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.26.3 h1:emf74GIQMTik01Aum9dPP0gAypL8JTLl/lHa4V9RFSU=
|
||||
k8s.io/api v0.26.3/go.mod h1:PXsqwPMXBSBcL1lJ9CYDKy7kIReUydukS5JiRlxC3qE=
|
||||
k8s.io/apiextensions-apiserver v0.26.3 h1:5PGMm3oEzdB1W/FTMgGIDmm100vn7IaUP5er36dB+YE=
|
||||
k8s.io/apiextensions-apiserver v0.26.3/go.mod h1:jdA5MdjNWGP+njw1EKMZc64xAT5fIhN6VJrElV3sfpQ=
|
||||
k8s.io/apimachinery v0.26.3 h1:dQx6PNETJ7nODU3XPtrwkfuubs6w7sX0M8n61zHIV/k=
|
||||
k8s.io/apimachinery v0.26.3/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I=
|
||||
k8s.io/client-go v0.26.3 h1:k1UY+KXfkxV2ScEL3gilKcF7761xkYsSD6BC9szIu8s=
|
||||
k8s.io/client-go v0.26.3/go.mod h1:ZPNu9lm8/dbRIPAgteN30RSXea6vrCpFvq+MateTUuQ=
|
||||
k8s.io/component-base v0.26.3 h1:oC0WMK/ggcbGDTkdcqefI4wIZRYdK3JySx9/HADpV0g=
|
||||
k8s.io/component-base v0.26.3/go.mod h1:5kj1kZYwSC6ZstHJN7oHBqcJC6yyn41eR+Sqa/mQc8E=
|
||||
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
|
||||
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg=
|
||||
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
|
||||
k8s.io/kubectl v0.26.3 h1:bZ5SgFyeEXw6XTc1Qji0iNdtqAC76lmeIIQULg2wNXM=
|
||||
k8s.io/kubectl v0.26.3/go.mod h1:02+gv7Qn4dupzN3fi/9OvqqdW+uG/4Zi56vc4Zmsp1g=
|
||||
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 h1:xMMXJlJbsU8w3V5N2FLDQ8YgU8s1EoULdbQBcAeNJkY=
|
||||
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.14.5 h1:6xaWFqzT5KuAQ9ufgUaj1G/+C4Y1GRkhrxl+BJ9i+5s=
|
||||
sigs.k8s.io/controller-runtime v0.14.5/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
|
||||
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
|
||||
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
|
||||
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc=
|
||||
k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8=
|
||||
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
|
||||
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
|
||||
k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk=
|
||||
k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
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/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
|
||||
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
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=
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,20 +22,19 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"github.com/1Password/onepassword-operator/pkg/logs"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
@@ -47,6 +46,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
var logDeployment = logf.Log.WithName("controller_deployment")
|
||||
@@ -55,13 +55,13 @@ var logDeployment = logf.Log.WithName("controller_deployment")
|
||||
type DeploymentReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
OpConnectClient connect.Client
|
||||
OpClient opclient.Client
|
||||
OpAnnotationRegExp *regexp.Regexp
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
@@ -77,7 +77,7 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
reqLogger.V(logs.DebugLevel).Info("Reconciling Deployment")
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
err := r.Get(context.Background(), req.NamespacedName, deployment)
|
||||
err := r.Get(ctx, req.NamespacedName, deployment)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
@@ -91,33 +91,38 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
//If the deployment is not being deleted
|
||||
if deployment.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// If the deployment is not being deleted
|
||||
if deployment.DeletionTimestamp.IsZero() {
|
||||
// Adds a finalizer to the deployment if one does not exist.
|
||||
// This is so we can handle cleanup of associated secrets properly
|
||||
if !utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
|
||||
deployment.ObjectMeta.Finalizers = append(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
if err = r.Update(context.Background(), deployment); err != nil {
|
||||
if !utils.ContainsString(deployment.Finalizers, finalizer) {
|
||||
deployment.Finalizers = append(deployment.Finalizers, finalizer)
|
||||
if err = r.Update(ctx, deployment); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
if err = r.handleApplyingDeployment(deployment, deployment.Namespace, annotations, req); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
if err = r.handleApplyingDeployment(ctx, deployment, deployment.Namespace, annotations, req); err != nil {
|
||||
if strings.Contains(err.Error(), "rate limit") {
|
||||
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
|
||||
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
|
||||
} else {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
// The deployment has been marked for deletion. If the one password
|
||||
// finalizer is found there are cleanup tasks to perform
|
||||
if utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
|
||||
if utils.ContainsString(deployment.Finalizers, finalizer) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -128,27 +133,28 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&appsv1.Deployment{}).
|
||||
Named("onepassword-deployment").
|
||||
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.ObjectMeta.Name = secretName
|
||||
kubernetesSecret.ObjectMeta.Namespace = deletedDeployment.Namespace
|
||||
kubernetesSecret.Name = secretName
|
||||
kubernetesSecret.Namespace = deletedDeployment.Namespace
|
||||
|
||||
if len(secretName) == 0 {
|
||||
return nil
|
||||
}
|
||||
updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret}
|
||||
|
||||
multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment)
|
||||
multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(ctx, updatedSecrets, *deletedDeployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only delete the associated kubernetes secret if it is not being used by other deployments
|
||||
if !multipleDeploymentsUsingSecret {
|
||||
if err = r.Delete(context.Background(), kubernetesSecret); err != nil {
|
||||
if err = r.Delete(ctx, kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
@@ -157,13 +163,13 @@ func (r *DeploymentReconciler) cleanupKubernetesSecretForDeployment(secretName s
|
||||
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{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(deletedDeployment.Namespace),
|
||||
}
|
||||
|
||||
err := r.List(context.Background(), deployments, opts...)
|
||||
err := r.List(ctx, deployments, opts...)
|
||||
if err != nil {
|
||||
logDeployment.Error(err, "Failed to list kubernetes deployments")
|
||||
return false, err
|
||||
@@ -179,12 +185,12 @@ func (r *DeploymentReconciler) areMultipleDeploymentsUsingSecret(updatedSecrets
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *DeploymentReconciler) removeOnePasswordFinalizerFromDeployment(deployment *appsv1.Deployment) error {
|
||||
deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
return r.Update(context.Background(), deployment)
|
||||
func (r *DeploymentReconciler) removeOnePasswordFinalizerFromDeployment(ctx context.Context, deployment *appsv1.Deployment) error {
|
||||
deployment.Finalizers = utils.RemoveString(deployment.Finalizers, finalizer)
|
||||
return r.Update(ctx, deployment)
|
||||
}
|
||||
|
||||
func (r *DeploymentReconciler) handleApplyingDeployment(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)
|
||||
|
||||
secretName := annotations[op.NameAnnotation]
|
||||
@@ -196,15 +202,15 @@ func (r *DeploymentReconciler) handleApplyingDeployment(deployment *appsv1.Deplo
|
||||
return nil
|
||||
}
|
||||
|
||||
item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, annotations[op.ItemPathAnnotation])
|
||||
item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, annotations[op.ItemPathAnnotation])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve item: %v", err)
|
||||
return fmt.Errorf("failed to retrieve item: %w", err)
|
||||
}
|
||||
|
||||
// Create owner reference.
|
||||
gvk, err := apiutil.GVKForObject(deployment, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not to retrieve group version kind: %v", err)
|
||||
return fmt.Errorf("could not to retrieve group version kind: %w", err)
|
||||
}
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
@@ -213,5 +219,5 @@ func (r *DeploymentReconciler) handleApplyingDeployment(deployment *appsv1.Deplo
|
||||
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)
|
||||
}
|
@@ -1,10 +1,7 @@
|
||||
package controllers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -17,6 +14,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,14 +24,13 @@ const (
|
||||
)
|
||||
|
||||
var _ = Describe("Deployment controller", func() {
|
||||
var ctx context.Context
|
||||
ctx := context.Background()
|
||||
var deploymentKey types.NamespacedName
|
||||
var secretKey types.NamespacedName
|
||||
var deploymentResource *appsv1.Deployment
|
||||
createdSecret := &v1.Secret{}
|
||||
|
||||
makeDeployment := func() {
|
||||
ctx = context.Background()
|
||||
|
||||
deploymentKey = types.NamespacedName{
|
||||
Name: deploymentName,
|
||||
@@ -85,38 +82,26 @@ var _ = Describe("Deployment controller", func() {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, secretKey, createdSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(createdSecret.Data).Should(Equal(item1.SecretData))
|
||||
}
|
||||
|
||||
cleanK8sResources := func() {
|
||||
// failed test runs that don't clean up leave resources behind.
|
||||
err := k8sClient.DeleteAllOf(context.Background(), &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace))
|
||||
err := k8sClient.DeleteAllOf(ctx, &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace))
|
||||
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())
|
||||
|
||||
err = k8sClient.DeleteAllOf(context.Background(), &appsv1.Deployment{}, client.InNamespace(namespace))
|
||||
err = k8sClient.DeleteAllOf(ctx, &appsv1.Deployment{}, client.InNamespace(namespace))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
mockGetItemFunc := func() {
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
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
|
||||
}
|
||||
// mock GetItemByID to return test item 'item1'
|
||||
mockGetItemByIDFunc.Return(item1.ToModel(), nil)
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
@@ -151,17 +136,10 @@ var _ = Describe("Deployment controller", func() {
|
||||
|
||||
It("Should update existing K8s Secret using deployment", func() {
|
||||
By("Updating secret")
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
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
|
||||
}
|
||||
|
||||
// mock GetItemByID to return test item 'item2'
|
||||
mockGetItemByIDFunc.Return(item2.ToModel(), nil)
|
||||
|
||||
Eventually(func() error {
|
||||
updatedDeployment := &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -209,10 +187,7 @@ var _ = Describe("Deployment controller", func() {
|
||||
updatedSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, secretKey, updatedSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(updatedSecret.Data).Should(Equal(item2.SecretData))
|
||||
})
|
||||
@@ -266,10 +241,7 @@ var _ = Describe("Deployment controller", func() {
|
||||
updatedSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, secretKey, updatedSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(updatedSecret.Data).Should(Equal(item1.SecretData))
|
||||
})
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,18 +22,19 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"github.com/1Password/onepassword-operator/pkg/logs"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -52,21 +53,22 @@ var finalizer = "onepassword.com/finalizer.secret"
|
||||
// OnePasswordItemReconciler reconciles a OnePasswordItem object
|
||||
type OnePasswordItemReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
OpConnectClient connect.Client
|
||||
Scheme *runtime.Scheme
|
||||
OpClient opclient.Client
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/finalizers,verbs=update
|
||||
|
||||
//+kubebuilder:rbac:groups="",resources=pods,verbs=get
|
||||
//+kubebuilder:rbac:groups="",resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets;namespaces,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=daemonsets;deployments;replicasets;statefulsets,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=replicasets;deployments,verbs=get
|
||||
//+kubebuilder:rbac:groups=apps,resourceNames=onepassword-connect-operator,resources=deployments/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups=onepassword.com,resources=*,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create
|
||||
// +kubebuilder:rbac:groups="",resources=pods,verbs=get
|
||||
// +kubebuilder:rbac:groups="",resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets;namespaces,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=apps,resources=daemonsets;deployments;replicasets;statefulsets,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=apps,resources=replicasets;deployments,verbs=get
|
||||
// +kubebuilder:rbac:groups=apps,resourceNames=onepassword-connect-operator,resources=deployments/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=onepassword.com,resources=*,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create
|
||||
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
@@ -82,7 +84,7 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
reqLogger.V(logs.DebugLevel).Info("Reconciling OnePasswordItem")
|
||||
|
||||
onepassworditem := &onepasswordv1.OnePasswordItem{}
|
||||
err := r.Get(context.Background(), req.NamespacedName, onepassworditem)
|
||||
err := r.Get(ctx, req.NamespacedName, onepassworditem)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
@@ -91,33 +93,39 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
}
|
||||
|
||||
// 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.
|
||||
// This is so we can handle cleanup of associated secrets properly
|
||||
if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||
onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer)
|
||||
if err = r.Update(context.Background(), onepassworditem); err != nil {
|
||||
if !utils.ContainsString(onepassworditem.Finalizers, finalizer) {
|
||||
onepassworditem.Finalizers = append(onepassworditem.Finalizers, finalizer)
|
||||
if err = r.Update(ctx, onepassworditem); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
err = r.handleOnePasswordItem(onepassworditem, req)
|
||||
if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil {
|
||||
err = r.handleOnePasswordItem(ctx, onepassworditem, req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "rate limit") {
|
||||
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
|
||||
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
|
||||
}
|
||||
}
|
||||
if updateStatusErr := r.updateStatus(ctx, onepassworditem, err); updateStatusErr != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr)
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// If one password finalizer exists then we must cleanup associated secrets
|
||||
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||
if utils.ContainsString(onepassworditem.Finalizers, finalizer) {
|
||||
|
||||
// Delete associated kubernetes secret
|
||||
if err = r.cleanupKubernetesSecret(onepassworditem); err != nil {
|
||||
if err = r.cleanupKubernetesSecret(ctx, onepassworditem); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -128,23 +136,16 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&onepasswordv1.OnePasswordItem{}).
|
||||
Named("onepassworditem").
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) removeFinalizer(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 {
|
||||
func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
kubernetesSecret := &corev1.Secret{}
|
||||
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name
|
||||
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace
|
||||
kubernetesSecret.Name = onePasswordItem.Name
|
||||
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) {
|
||||
return err
|
||||
}
|
||||
@@ -152,26 +153,26 @@ func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *one
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(opSecret *onepasswordv1.OnePasswordItem) error {
|
||||
opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer)
|
||||
return r.Update(context.Background(), opSecret)
|
||||
func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
onePasswordItem.Finalizers = utils.RemoveString(onePasswordItem.Finalizers, finalizer)
|
||||
return r.Update(ctx, onePasswordItem)
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) handleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, req ctrl.Request) error {
|
||||
func (r *OnePasswordItemReconciler) handleOnePasswordItem(ctx context.Context, resource *onepasswordv1.OnePasswordItem, _ ctrl.Request) error {
|
||||
secretName := resource.GetName()
|
||||
labels := resource.Labels
|
||||
secretType := resource.Type
|
||||
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
|
||||
|
||||
item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, resource.Spec.ItemPath)
|
||||
item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, resource.Spec.ItemPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve item: %v", err)
|
||||
return fmt.Errorf("failed to retrieve item: %w", err)
|
||||
}
|
||||
|
||||
// Create owner reference.
|
||||
gvk, err := apiutil.GVKForObject(resource, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not to retrieve group version kind: %v", err)
|
||||
return fmt.Errorf("could not to retrieve group version kind: %w", err)
|
||||
}
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
@@ -180,10 +181,10 @@ func (r *OnePasswordItemReconciler) handleOnePasswordItem(resource *onepasswordv
|
||||
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)
|
||||
updatedCondition := existingCondition
|
||||
if err != nil {
|
||||
@@ -199,7 +200,7 @@ func (r *OnePasswordItemReconciler) updateStatus(resource *onepasswordv1.OnePass
|
||||
}
|
||||
|
||||
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 {
|
@@ -1,10 +1,8 @@
|
||||
package controllers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
"fmt"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -16,6 +14,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,17 +31,8 @@ var _ = Describe("OnePasswordItem controller", func() {
|
||||
err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
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
|
||||
}
|
||||
item := item1.ToModel()
|
||||
mockGetItemByIDFunc.Return(item, nil)
|
||||
})
|
||||
|
||||
Context("Happy path", func() {
|
||||
@@ -71,20 +61,14 @@ var _ = Describe("OnePasswordItem controller", func() {
|
||||
created := &onepasswordv1.OnePasswordItem{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, created)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
By("Creating the K8s secret successfully")
|
||||
createdSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, createdSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(createdSecret.Data).Should(Equal(item1.SecretData))
|
||||
|
||||
@@ -99,27 +83,20 @@ var _ = Describe("OnePasswordItem controller", func() {
|
||||
"password": []byte("##newPassword##"),
|
||||
"extraField": []byte("dev"),
|
||||
}
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
for k, v := range newData {
|
||||
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
|
||||
}
|
||||
item.Version = item1.Version + 1
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
|
||||
item := item2.ToModel()
|
||||
for k, v := range newData {
|
||||
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
|
||||
}
|
||||
mockGetItemByIDFunc.Return(item, nil)
|
||||
|
||||
_, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
updatedSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, updatedSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(updatedSecret.Data).Should(Equal(newDataByte))
|
||||
|
||||
@@ -178,18 +155,11 @@ var _ = Describe("OnePasswordItem controller", func() {
|
||||
"ice-cream-type": []byte(iceCream),
|
||||
}
|
||||
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
item.Title = "!my sECReT it3m%"
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
for k, v := range testData {
|
||||
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
|
||||
}
|
||||
item.Version = item1.Version + 1
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
item := item2.ToModel()
|
||||
for k, v := range testData {
|
||||
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
|
||||
}
|
||||
mockGetItemByIDFunc.Return(item, nil)
|
||||
|
||||
By("Creating a new OnePasswordItem successfully")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
|
||||
@@ -197,20 +167,14 @@ var _ = Describe("OnePasswordItem controller", func() {
|
||||
created := &onepasswordv1.OnePasswordItem{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, created)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
By("Creating the K8s secret successfully")
|
||||
createdSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, createdSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(createdSecret.Data).Should(Equal(expectedData))
|
||||
|
||||
@@ -319,13 +283,55 @@ var _ = Describe("OnePasswordItem controller", func() {
|
||||
secret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, secret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(secret.Type).Should(Equal(v1.SecretType(customType)))
|
||||
})
|
||||
|
||||
It("Should handle 1Password Item with a file and populate secret correctly", func() {
|
||||
ctx := context.Background()
|
||||
spec := onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: item1.Path,
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Name: "item-with-file",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
toCreate := &onepasswordv1.OnePasswordItem{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
fileContent := []byte("dummy-cert-content")
|
||||
item := item1.ToModel()
|
||||
item.Files = []model.File{
|
||||
{
|
||||
ID: "file-id-123",
|
||||
Name: "server.crt",
|
||||
ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/file-id-123/content", item.VaultID, item.ID),
|
||||
},
|
||||
}
|
||||
item.Files[0].SetContent(fileContent)
|
||||
|
||||
mockGetItemByIDFunc.Return(item, nil)
|
||||
mockGetItemByIDFunc.On("GetFileContent", item.VaultID, item.ID, "file-id-123").Return(fileContent, nil)
|
||||
|
||||
By("Creating a new OnePasswordItem with file successfully")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
|
||||
|
||||
createdSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, createdSecret)
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
Expect(createdSecret.Data).Should(HaveKeyWithValue("server.crt", fileContent))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Unhappy path", func() {
|
||||
@@ -355,20 +361,14 @@ var _ = Describe("OnePasswordItem controller", func() {
|
||||
secret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, secret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
By("Failing to update K8s secret")
|
||||
Eventually(func() bool {
|
||||
secret.Type = v1.SecretTypeBasicAuth
|
||||
err := k8sClient.Update(ctx, secret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeFalse())
|
||||
})
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,19 +22,19 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
@@ -45,7 +45,9 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
//+kubebuilder:scaffold:imports
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
@@ -78,8 +80,11 @@ var (
|
||||
cancel context.CancelFunc
|
||||
onePasswordItemReconciler *OnePasswordItemReconciler
|
||||
deploymentReconciler *DeploymentReconciler
|
||||
mockGetItemByIDFunc *mock.Call
|
||||
|
||||
item1 = &TestItem{
|
||||
ItemID: "nwrhuano7bcwddcviubpp4mhfq",
|
||||
VaultID: "hfnjvi6aymbsnfc2xeeoheizda",
|
||||
Name: "test-item",
|
||||
Version: 123,
|
||||
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizda/items/nwrhuano7bcwddcviubpp4mhfq",
|
||||
@@ -94,6 +99,8 @@ var (
|
||||
}
|
||||
|
||||
item2 = &TestItem{
|
||||
ItemID: "nwrhuano7bcwddcviubpp4mhf2",
|
||||
VaultID: "hfnjvi6aymbsnfc2xeeoheizd2",
|
||||
Name: "test-item2",
|
||||
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizd2/items/nwrhuano7bcwddcviubpp4mhf2",
|
||||
Version: 456,
|
||||
@@ -109,6 +116,8 @@ var (
|
||||
)
|
||||
|
||||
type TestItem struct {
|
||||
ItemID string
|
||||
VaultID string
|
||||
Name string
|
||||
Version int
|
||||
Path string
|
||||
@@ -116,6 +125,20 @@ type TestItem struct {
|
||||
SecretData map[string][]byte
|
||||
}
|
||||
|
||||
func (ti *TestItem) ToModel() *model.Item {
|
||||
item := &model.Item{}
|
||||
item.Version = ti.Version
|
||||
item.VaultID = ti.VaultID
|
||||
item.ID = ti.ItemID
|
||||
|
||||
item.Fields = []model.ItemField{}
|
||||
for k, v := range ti.Data {
|
||||
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
@@ -129,10 +152,15 @@ var _ = BeforeSuite(func() {
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
}
|
||||
|
||||
// Retrieve the first found binary directory to allow running tests from IDEs
|
||||
if getFirstFoundEnvTestBinaryDir() != "" {
|
||||
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
|
||||
}
|
||||
|
||||
var err error
|
||||
// cfg is defined in this file globally.
|
||||
cfg, err = testEnv.Start()
|
||||
@@ -142,7 +170,7 @@ var _ = BeforeSuite(func() {
|
||||
err = onepasswordcomv1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
//+kubebuilder:scaffold:scheme
|
||||
// +kubebuilder:scaffold:scheme
|
||||
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@@ -153,12 +181,13 @@ var _ = BeforeSuite(func() {
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
opConnectClient := &mocks.TestClient{}
|
||||
mockOpClient := &mocks.TestClient{}
|
||||
mockGetItemByIDFunc = mockOpClient.On("GetItemByID", mock.Anything, mock.Anything)
|
||||
|
||||
onePasswordItemReconciler = &OnePasswordItemReconciler{
|
||||
Client: k8sManager.GetClient(),
|
||||
Scheme: k8sManager.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
Client: k8sManager.GetClient(),
|
||||
Scheme: k8sManager.GetScheme(),
|
||||
OpClient: mockOpClient,
|
||||
}
|
||||
err = (onePasswordItemReconciler).SetupWithManager(k8sManager)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@@ -167,7 +196,7 @@ var _ = BeforeSuite(func() {
|
||||
deploymentReconciler = &DeploymentReconciler{
|
||||
Client: k8sManager.GetClient(),
|
||||
Scheme: k8sManager.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
OpClient: mockOpClient,
|
||||
OpAnnotationRegExp: r,
|
||||
}
|
||||
err = (deploymentReconciler).SetupWithManager(k8sManager)
|
||||
@@ -187,3 +216,26 @@ var _ = AfterSuite(func() {
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
|
||||
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
|
||||
// controller-runtime. When running tests directly (e.g., via an IDE) without using
|
||||
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
|
||||
//
|
||||
// This function streamlines the process by finding the required binaries, similar to
|
||||
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
|
||||
// properly set up, run 'make setup-envtest' beforehand.
|
||||
func getFirstFoundEnvTestBinaryDir() string {
|
||||
basePath := filepath.Join("..", "..", "bin", "k8s")
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
logf.Log.Error(err, "Failed to read directory", "path", basePath)
|
||||
return ""
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
return filepath.Join(basePath, entry.Name())
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
286
main.go
286
main.go
@@ -1,286 +0,0 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
k8sruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
"github.com/1Password/onepassword-operator/controllers"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
"github.com/1Password/onepassword-operator/version"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = k8sruntime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
const (
|
||||
envPollingIntervalVariable = "POLLING_INTERVAL"
|
||||
manageConnect = "MANAGE_CONNECT"
|
||||
restartDeploymentsEnvVariable = "AUTO_RESTART"
|
||||
defaultPollingInterval = 600
|
||||
|
||||
annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
|
||||
)
|
||||
|
||||
// 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() {
|
||||
setupLog.Info(fmt.Sprintf("Operator Version: %s", version.OperatorVersion))
|
||||
setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
|
||||
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
|
||||
setupLog.Info(fmt.Sprintf("Version of operator-sdk: %v", version.OperatorSDKVersion))
|
||||
}
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(onepasswordcomv1.AddToScheme(scheme))
|
||||
//+kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
printVersion()
|
||||
|
||||
watchNamespace, err := getWatchNamespace()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to get WatchNamespace, "+
|
||||
"the manager will watch and manage resources in all namespaces")
|
||||
}
|
||||
|
||||
deploymentNamespace, err := utils.GetOperatorNamespace()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "Failed to get namespace")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
options := ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Namespace: watchNamespace,
|
||||
MetricsBindAddress: metricsAddr,
|
||||
Port: 9443,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "c26807fd.onepassword.com",
|
||||
}
|
||||
|
||||
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
|
||||
if strings.Contains(watchNamespace, ",") {
|
||||
setupLog.Info("manager set up with multiple namespaces", "namespaces", watchNamespace)
|
||||
// configure cluster-scoped with MultiNamespacedCacheBuilder
|
||||
options.Namespace = ""
|
||||
options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(watchNamespace, ","))
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup One Password Client
|
||||
opConnectClient, err := connect.NewClientFromEnvironment()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to create Connect client")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controllers.OnePasswordItemReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
r, _ := regexp.Compile(annotationRegExpString)
|
||||
if err = (&controllers.DeploymentReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
OpAnnotationRegExp: r,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Deployment")
|
||||
os.Exit(1)
|
||||
}
|
||||
//+kubebuilder:scaffold:builder
|
||||
|
||||
//Setup 1PasswordConnect
|
||||
if shouldManageConnect() {
|
||||
setupLog.Info("Automated Connect Management Enabled")
|
||||
go func() {
|
||||
connectStarted := false
|
||||
for connectStarted == false {
|
||||
err := op.SetupConnect(mgr.GetClient(), deploymentNamespace)
|
||||
// Cache Not Started is an acceptable error. Retry until cache is started.
|
||||
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err == nil {
|
||||
connectStarted = true
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
setupLog.Info("Automated Connect Management Disabled")
|
||||
}
|
||||
|
||||
// Setup update secrets task
|
||||
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments())
|
||||
done := make(chan bool)
|
||||
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := updatedSecretsPoller.UpdateKubernetesSecretsTask()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "error running update kubernetes secret task")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// getWatchNamespace returns the Namespace the operator should be watching for changes
|
||||
func getWatchNamespace() (string, error) {
|
||||
// WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE
|
||||
// which specifies the Namespace to watch.
|
||||
// An empty value means the operator is running with cluster scope.
|
||||
var watchNamespaceEnvVar = "WATCH_NAMESPACE"
|
||||
|
||||
ns, found := os.LookupEnv(watchNamespaceEnvVar)
|
||||
if !found {
|
||||
return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar)
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func shouldManageConnect() bool {
|
||||
shouldManageConnect, found := os.LookupEnv(manageConnect)
|
||||
if found {
|
||||
shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldManageConnectBool
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldAutoRestartDeployments() bool {
|
||||
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
|
||||
if found {
|
||||
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldAutoRestartDeploymentsBool
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getPollingIntervalForUpdatingSecrets() time.Duration {
|
||||
timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable)
|
||||
if found {
|
||||
timeInSeconds, err := strconv.Atoi(timeInSecondsString)
|
||||
if err == nil {
|
||||
return time.Duration(timeInSeconds) * time.Second
|
||||
}
|
||||
setupLog.Info("Invalid value set for polling interval. Must be a valid integer.")
|
||||
}
|
||||
|
||||
setupLog.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval))
|
||||
return time.Duration(defaultPollingInterval) * time.Second
|
||||
}
|
@@ -2,20 +2,16 @@ package kubernetessecrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"reflect"
|
||||
|
||||
errs "errors"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
||||
@@ -30,33 +26,45 @@ const VersionAnnotation = OnepasswordPrefix + "/item-version"
|
||||
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
|
||||
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
|
||||
|
||||
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)
|
||||
secretAnnotations := map[string]string{
|
||||
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 != "" {
|
||||
_, err := utils.StringToBool(autoRestart)
|
||||
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
|
||||
}
|
||||
|
||||
// "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{}
|
||||
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
err := kubeClient.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
|
||||
if err != nil && apierrors.IsNotFound(err) {
|
||||
log.Info(fmt.Sprintf("Creating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
|
||||
return kubeClient.Create(context.Background(), secret)
|
||||
return kubeClient.Create(ctx, secret)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,20 +87,29 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
|
||||
currentLabels := currentSecret.Labels
|
||||
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) {
|
||||
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
|
||||
currentSecret.ObjectMeta.Annotations = secretAnnotations
|
||||
currentSecret.ObjectMeta.Labels = labels
|
||||
currentSecret.Annotations = secretAnnotations
|
||||
currentSecret.Labels = labels
|
||||
currentSecret.Data = secret.Data
|
||||
if err := kubeClient.Update(context.Background(), currentSecret); err != nil {
|
||||
return fmt.Errorf("Kubernetes secret update failed: %w", err)
|
||||
if err := kubeClient.Update(ctx, currentSecret); err != nil {
|
||||
return fmt.Errorf("kubernetes secret update failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation]))
|
||||
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists",
|
||||
secret.Name, secret.Annotations[VersionAnnotation],
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, 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
|
||||
if ownerRef != nil {
|
||||
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{}
|
||||
for i := 0; i < len(fields); i++ {
|
||||
if fields[i].Value != "" {
|
||||
@@ -124,7 +141,7 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepass
|
||||
for _, file := range files {
|
||||
content, err := file.Content()
|
||||
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
|
||||
}
|
||||
if content != nil {
|
||||
|
@@ -6,37 +6,44 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
||||
"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) {
|
||||
ctx := context.Background()
|
||||
secretName := "test-secret-name"
|
||||
namespace := "test"
|
||||
namespace := testNamespace
|
||||
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
item.VaultID = testVaultUUID
|
||||
item.ID = testItemUUID
|
||||
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
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 {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
createdSecret := &corev1.Secret{}
|
||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
err = kubeClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Secret was not created: %v", err)
|
||||
@@ -46,14 +53,15 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
secretName := "test-secret-name"
|
||||
namespace := "test"
|
||||
namespace := testNamespace
|
||||
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
item.VaultID = testVaultUUID
|
||||
item.ID = testItemUUID
|
||||
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
@@ -65,15 +73,19 @@ func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
|
||||
Name: "test-deployment",
|
||||
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 {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
createdSecret := &corev1.Secret{}
|
||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
err = kubeClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Check owner references.
|
||||
gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences
|
||||
gotOwnerRefs := createdSecret.OwnerReferences
|
||||
if len(gotOwnerRefs) != 1 {
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
secretName := "test-secret-update"
|
||||
namespace := "test"
|
||||
namespace := testNamespace
|
||||
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
item.VaultID = testVaultUUID
|
||||
item.ID = testItemUUID
|
||||
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
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 {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Updating kubernetes secret with new item
|
||||
newItem := onepassword.Item{}
|
||||
newItem := model.Item{}
|
||||
newItem.Fields = generateFields(6)
|
||||
newItem.Version = 456
|
||||
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||
newItem.VaultID = testVaultUUID
|
||||
newItem.ID = testItemUUID
|
||||
err = CreateKubernetesSecretFromItem(ctx, kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation,
|
||||
secretLabels, secretType, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
updatedSecret := &corev1.Secret{}
|
||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, updatedSecret)
|
||||
err = kubeClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, updatedSecret)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Secret was not found: %v", err)
|
||||
@@ -147,7 +162,7 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
annotations := map[string]string{
|
||||
annotationKey: annotationValue,
|
||||
}
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
labels := map[string]string{}
|
||||
secretType := ""
|
||||
@@ -173,10 +188,10 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
|
||||
"annotationKey": "annotationValue",
|
||||
}
|
||||
labels := map[string]string{}
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
secretType := ""
|
||||
|
||||
item.Fields = []*onepassword.ItemField{
|
||||
item.Fields = []model.ItemField{
|
||||
{
|
||||
Label: "label w%th invalid ch!rs-",
|
||||
Value: "value1",
|
||||
@@ -206,25 +221,27 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
secretName := "tls-test-secret-name"
|
||||
namespace := "test"
|
||||
namespace := testNamespace
|
||||
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
item.VaultID = testVaultUUID
|
||||
item.ID = testItemUUID
|
||||
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
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 {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
createdSecret := &corev1.Secret{}
|
||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
err = kubeClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Secret was not created: %v", err)
|
||||
@@ -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])
|
||||
if err != nil {
|
||||
t.Errorf("Was unable to parse Item Path")
|
||||
}
|
||||
if actualVaultId != item.Vault.ID {
|
||||
t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId)
|
||||
if actualVaultId != item.VaultID {
|
||||
t.Errorf("Expected annotation vault id to be %v but was %v", item.VaultID, actualVaultId)
|
||||
}
|
||||
if actualItemId != item.ID {
|
||||
t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId)
|
||||
@@ -251,11 +268,13 @@ func compareAnnotationsToItem(annotations map[string]string, item onepassword.It
|
||||
}
|
||||
|
||||
if annotations[RestartDeploymentsAnnotation] != "false" {
|
||||
t.Errorf("Expected restart deployments annotation to be %v but was %v", restartDeploymentAnnotation, RestartDeploymentsAnnotation)
|
||||
t.Errorf("Expected restart deployments annotation to be %v but was %v",
|
||||
restartDeploymentAnnotation, RestartDeploymentsAnnotation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func compareFields(actualFields []*onepassword.ItemField, secretData map[string][]byte, t *testing.T) {
|
||||
func compareFields(actualFields []model.ItemField, secretData map[string][]byte, t *testing.T) {
|
||||
for i := 0; i < len(actualFields); i++ {
|
||||
value, found := secretData[actualFields[i].Label]
|
||||
if !found {
|
||||
@@ -267,14 +286,13 @@ func compareFields(actualFields []*onepassword.ItemField, secretData map[string]
|
||||
}
|
||||
}
|
||||
|
||||
func generateFields(numToGenerate int) []*onepassword.ItemField {
|
||||
fields := []*onepassword.ItemField{}
|
||||
func generateFields(numToGenerate int) []model.ItemField {
|
||||
fields := []model.ItemField{}
|
||||
for i := 0; i < numToGenerate; i++ {
|
||||
field := onepassword.ItemField{
|
||||
fields = append(fields, model.ItemField{
|
||||
Label: "key" + fmt.Sprint(i),
|
||||
Value: "value" + fmt.Sprint(i),
|
||||
}
|
||||
fields = append(fields, &field)
|
||||
})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -284,7 +302,10 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
|
||||
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
|
||||
return splitPath[1], splitPath[3], nil
|
||||
}
|
||||
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
|
||||
return "", "", fmt.Errorf(
|
||||
"%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`",
|
||||
path,
|
||||
)
|
||||
}
|
||||
|
||||
func validLabel(v string) bool {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package logs
|
||||
|
||||
// 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
|
||||
const (
|
||||
ErrorLevel = -2
|
||||
|
@@ -1,151 +1,39 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"context"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
|
||||
)
|
||||
|
||||
type TestClient struct {
|
||||
GetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
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
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
var (
|
||||
DoGetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
DoGetVaultFunc func(uuid string) (*onepassword.Vault, error)
|
||||
DoGetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error)
|
||||
DoGetVaultByTitleFunc func(title string) (*onepassword.Vault, error)
|
||||
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 (tc *TestClient) GetItemByID(ctx context.Context, vaultID, itemID string) (*model.Item, error) {
|
||||
args := tc.Called(vaultID, itemID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*model.Item), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
|
||||
return DoGetVaultsByTitleFunc(title)
|
||||
func (tc *TestClient) GetItemsByTitle(ctx context.Context, vaultID, itemTitle string) ([]model.Item, error) {
|
||||
args := tc.Called(vaultID, itemTitle)
|
||||
return args.Get(0).([]model.Item), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetVault(vaultQuery string) (*onepassword.Vault, error) {
|
||||
return DoGetVaultFunc(vaultQuery)
|
||||
func (tc *TestClient) GetFileContent(ctx context.Context, vaultID, itemID, fileID string) ([]byte, error) {
|
||||
args := tc.Called(vaultID, itemID, fileID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]byte), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetVaultByUUID(uuid string) (*onepassword.Vault, error) {
|
||||
return DoGetVaultByUUIDFunc(uuid)
|
||||
}
|
||||
|
||||
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)
|
||||
func (tc *TestClient) GetVaultsByTitle(ctx context.Context, title string) ([]model.Vault, error) {
|
||||
args := tc.Called(title)
|
||||
return args.Get(0).([]model.Vault), args.Error(1)
|
||||
}
|
||||
|
@@ -45,13 +45,14 @@ func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[
|
||||
|
||||
func AreAnnotationsUsingSecrets(annotations map[string]string, secrets map[string]*corev1.Secret) bool {
|
||||
_, ok := secrets[annotations[NameAnnotation]]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return ok
|
||||
}
|
||||
|
||||
func AppendAnnotationUpdatedSecret(annotations map[string]string, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret {
|
||||
func AppendAnnotationUpdatedSecret(
|
||||
annotations map[string]string,
|
||||
secrets map[string]*corev1.Secret,
|
||||
updatedDeploymentSecrets map[string]*corev1.Secret,
|
||||
) map[string]*corev1.Secret {
|
||||
secret, ok := secrets[annotations[NameAnnotation]]
|
||||
if ok {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
|
@@ -80,7 +80,7 @@ func TestGetNoAnnotationsForDeployment(t *testing.T) {
|
||||
}
|
||||
|
||||
numAnnotations := len(filteredAnnotations)
|
||||
if 0 != numAnnotations {
|
||||
if numAnnotations != 0 {
|
||||
t.Errorf("Expected %v annotations got %v", 0, numAnnotations)
|
||||
}
|
||||
}
|
||||
|
56
pkg/onepassword/client/client.go
Normal file
56
pkg/onepassword/client/client.go
Normal 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")
|
||||
}
|
104
pkg/onepassword/client/connect/connect.go
Normal file
104
pkg/onepassword/client/connect/connect.go
Normal 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
|
||||
}
|
241
pkg/onepassword/client/connect/connect_test.go
Normal file
241
pkg/onepassword/client/connect/connect_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
97
pkg/onepassword/client/sdk/sdk.go
Normal file
97
pkg/onepassword/client/sdk/sdk.go
Normal 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
|
||||
}
|
288
pkg/onepassword/client/sdk/sdk_test.go
Normal file
288
pkg/onepassword/client/sdk/sdk_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
110
pkg/onepassword/client/testing/item.go
Normal file
110
pkg/onepassword/client/testing/item.go
Normal 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)
|
||||
}
|
134
pkg/onepassword/client/testing/mock/connect.go
Normal file
134
pkg/onepassword/client/testing/mock/connect.go
Normal 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")
|
||||
}
|
97
pkg/onepassword/client/testing/mock/sdk.go
Normal file
97
pkg/onepassword/client/testing/mock/sdk.go
Normal 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)
|
||||
}
|
@@ -15,16 +15,16 @@ import (
|
||||
)
|
||||
|
||||
var logConnectSetup = logf.Log.WithName("ConnectSetup")
|
||||
var deploymentPath = "config/connect/deployment.yaml"
|
||||
var servicePath = "config/connect/service.yaml"
|
||||
var deploymentPath = "../config/connect/deployment.yaml"
|
||||
var servicePath = "../config/connect/service.yaml"
|
||||
|
||||
func SetupConnect(kubeClient client.Client, deploymentNamespace string) error {
|
||||
err := setupService(kubeClient, servicePath, deploymentNamespace)
|
||||
func SetupConnect(ctx context.Context, kubeClient client.Client, deploymentNamespace string) error {
|
||||
err := setupService(ctx, kubeClient, servicePath, deploymentNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace)
|
||||
err = setupDeployment(ctx, kubeClient, deploymentPath, deploymentNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -32,27 +32,40 @@ func SetupConnect(kubeClient client.Client, deploymentNamespace string) error {
|
||||
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{}
|
||||
|
||||
// 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 errors.IsNotFound(err) {
|
||||
logConnectSetup.Info("No existing Connect deployment found. Creating Deployment")
|
||||
return createDeployment(kubeClient, deploymentPath, deploymentNamespace)
|
||||
return createDeployment(ctx, kubeClient, deploymentPath, deploymentNamespace)
|
||||
}
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = kubeClient.Create(context.Background(), deployment)
|
||||
err = kubeClient.Create(ctx, deployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,21 +91,29 @@ func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*
|
||||
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{}
|
||||
|
||||
//check if service has already been created
|
||||
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService)
|
||||
// check if service has already been created
|
||||
err := kubeClient.Get(ctx, types.NamespacedName{
|
||||
Name: "onepassword-connect",
|
||||
Namespace: deploymentNamespace,
|
||||
}, existingService)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logConnectSetup.Info("No existing Connect service found. Creating Service")
|
||||
return createService(kubeClient, servicePath, deploymentNamespace)
|
||||
return createService(ctx, kubeClient, servicePath, deploymentNamespace)
|
||||
}
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -108,7 +129,7 @@ func createService(kubeClient client.Client, servicePath string, deploymentNames
|
||||
return err
|
||||
}
|
||||
|
||||
err = kubeClient.Create(context.Background(), service)
|
||||
err = kubeClient.Create(ctx, service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import (
|
||||
var defaultNamespacedName = types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}
|
||||
|
||||
func TestServiceSetup(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Register operator types with the runtime scheme.
|
||||
s := scheme.Scheme
|
||||
@@ -25,7 +26,7 @@ func TestServiceSetup(t *testing.T) {
|
||||
// Create a fake client to mock API calls.
|
||||
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 {
|
||||
t.Errorf("Error Setting Up Connect: %v", err)
|
||||
@@ -33,13 +34,14 @@ func TestServiceSetup(t *testing.T) {
|
||||
|
||||
// check that service was created
|
||||
service := &corev1.Service{}
|
||||
err = client.Get(context.TODO(), defaultNamespacedName, service)
|
||||
err = client.Get(ctx, defaultNamespacedName, service)
|
||||
if err != nil {
|
||||
t.Errorf("Error Setting Up Connect service: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeploymentSetup(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Register operator types with the runtime scheme.
|
||||
s := scheme.Scheme
|
||||
@@ -50,7 +52,7 @@ func TestDeploymentSetup(t *testing.T) {
|
||||
// Create a fake client to mock API calls.
|
||||
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 {
|
||||
t.Errorf("Error Setting Up Connect: %v", err)
|
||||
@@ -58,7 +60,7 @@ func TestDeploymentSetup(t *testing.T) {
|
||||
|
||||
// check that deployment was created
|
||||
deployment := &appsv1.Deployment{}
|
||||
err = client.Get(context.TODO(), defaultNamespacedName, deployment)
|
||||
err = client.Get(ctx, defaultNamespacedName, deployment)
|
||||
if err != nil {
|
||||
t.Errorf("Error Setting Up Connect deployment: %v", err)
|
||||
}
|
||||
|
@@ -28,7 +28,11 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string
|
||||
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++ {
|
||||
envVariables := containers[i].Env
|
||||
for j := 0; j < len(envVariables); j++ {
|
||||
@@ -42,7 +46,7 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st
|
||||
envFromVariables := containers[i].EnvFrom
|
||||
for j := 0; j < len(envFromVariables); j++ {
|
||||
if envFromVariables[j].SecretRef != nil {
|
||||
secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name]
|
||||
secret, ok := secrets[envFromVariables[j].SecretRef.Name]
|
||||
if ok {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
}
|
||||
|
@@ -9,10 +9,15 @@ func IsDeploymentUsingSecrets(deployment *appsv1.Deployment, secrets map[string]
|
||||
volumes := deployment.Spec.Template.Spec.Volumes
|
||||
containers := deployment.Spec.Template.Spec.Containers
|
||||
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
|
||||
containers := deployment.Spec.Template.Spec.Containers
|
||||
containers = append(containers, deployment.Spec.Template.Spec.InitContainers...)
|
||||
|
@@ -1,42 +1,44 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
|
||||
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")
|
||||
|
||||
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
|
||||
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
|
||||
func GetOnePasswordItemByPath(ctx context.Context, opClient opclient.Client, path string) (*model.Item, error) {
|
||||
vaultNameOrID, itemNameOrID, err := ParseVaultAndItemFromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vaultId, err := getVaultId(opConnectClient, vaultValue)
|
||||
vaultID, err := getVaultID(ctx, opClient, vaultNameOrID)
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
_, err := opConnectClient.GetFileContent(file)
|
||||
for i, file := range item.Files {
|
||||
content, err := opClient.GetFileContent(ctx, vaultID, itemID, file.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Files[i].SetContent(content)
|
||||
}
|
||||
|
||||
return item, nil
|
||||
@@ -47,18 +49,21 @@ func ParseVaultAndItemFromPath(path string) (string, string, error) {
|
||||
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
|
||||
return splitPath[1], splitPath[3], nil
|
||||
}
|
||||
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
|
||||
return "", "", fmt.Errorf(
|
||||
"%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`",
|
||||
path,
|
||||
)
|
||||
}
|
||||
|
||||
func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {
|
||||
if !IsValidClientUUID(vaultIdentifier) {
|
||||
vaults, err := client.GetVaultsByTitle(vaultIdentifier)
|
||||
func getVaultID(ctx context.Context, client opclient.Client, vaultNameOrID string) (string, error) {
|
||||
if !IsValidClientUUID(vaultNameOrID) {
|
||||
vaults, err := client.GetVaultsByTitle(ctx, vaultNameOrID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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]
|
||||
@@ -68,22 +73,24 @@ func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {
|
||||
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) {
|
||||
if !IsValidClientUUID(itemIdentifier) {
|
||||
items, err := client.GetItemsByTitle(itemIdentifier, vaultId)
|
||||
func getItemID(ctx context.Context, client opclient.Client, vaultId, itemNameOrID string) (string, error) {
|
||||
if !IsValidClientUUID(itemNameOrID) {
|
||||
items, err := client.GetItemsByTitle(ctx, vaultId, itemNameOrID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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]
|
||||
@@ -93,9 +100,11 @@ func getItemId(client connect.Client, itemIdentifier string, vaultId string) (st
|
||||
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
|
||||
}
|
||||
|
27
pkg/onepassword/model/file.go
Normal file
27
pkg/onepassword/model/file.go
Normal 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
|
||||
}
|
92
pkg/onepassword/model/item.go
Normal file
92
pkg/onepassword/model/item.go
Normal 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
|
||||
}
|
7
pkg/onepassword/model/item_field.go
Normal file
7
pkg/onepassword/model/item_field.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
// ItemField Representation of a single field on an Item
|
||||
type ItemField struct {
|
||||
Label string
|
||||
Value string
|
||||
}
|
108
pkg/onepassword/model/item_test.go
Normal file
108
pkg/onepassword/model/item_test.go
Normal 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)
|
||||
}
|
23
pkg/onepassword/model/vault.go
Normal file
23
pkg/onepassword/model/vault.go
Normal 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
|
||||
}
|
37
pkg/onepassword/model/vault_test.go
Normal file
37
pkg/onepassword/model/vault_test.go
Normal 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)
|
||||
}
|
@@ -8,52 +8,59 @@ import (
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"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/connect-sdk-go/connect"
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
const envHostVariable = "OP_HOST"
|
||||
// const envHostVariable = "OP_HOST"
|
||||
const lockTag = "operator.1password.io:ignore-secret"
|
||||
|
||||
var log = logf.Log.WithName("update_op_kubernetes_secrets_task")
|
||||
|
||||
func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler {
|
||||
func NewManager(
|
||||
kubernetesClient client.Client,
|
||||
opClient opclient.Client,
|
||||
shouldAutoRestartDeploymentsGlobal bool,
|
||||
) *SecretUpdateHandler {
|
||||
return &SecretUpdateHandler{
|
||||
client: kubernetesClient,
|
||||
opConnectClient: opConnectClient,
|
||||
opClient: opClient,
|
||||
shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal,
|
||||
}
|
||||
}
|
||||
|
||||
type SecretUpdateHandler struct {
|
||||
client client.Client
|
||||
opConnectClient connect.Client
|
||||
opClient opclient.Client
|
||||
shouldAutoRestartDeploymentsGlobal bool
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error {
|
||||
updatedKubernetesSecrets, err := h.updateKubernetesSecrets()
|
||||
func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask(ctx context.Context) error {
|
||||
updatedKubernetesSecrets, err := h.updateKubernetesSecrets(ctx)
|
||||
if err != nil {
|
||||
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
|
||||
if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
deployments := &appsv1.DeploymentList{}
|
||||
err := h.client.List(context.Background(), deployments)
|
||||
err := h.client.List(ctx, deployments)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to list kubernetes deployments")
|
||||
return err
|
||||
@@ -63,7 +70,7 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
|
||||
return nil
|
||||
}
|
||||
|
||||
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap()
|
||||
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,32 +85,38 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
|
||||
}
|
||||
for _, secret := range updatedDeploymentSecrets {
|
||||
if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) {
|
||||
h.restartDeployment(deployment)
|
||||
h.restartDeployment(ctx, deployment)
|
||||
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
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) {
|
||||
log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace))
|
||||
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,
|
||||
))
|
||||
if deployment.Spec.Template.Annotations == nil {
|
||||
deployment.Spec.Template.Annotations = map[string]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 {
|
||||
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{}
|
||||
err := h.client.List(context.Background(), secrets)
|
||||
err := h.client.List(ctx, secrets)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to list kubernetes secrets")
|
||||
return nil, err
|
||||
@@ -121,22 +134,28 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
|
||||
|
||||
OnePasswordItemPath := h.getPathFromOnePasswordItem(secret)
|
||||
|
||||
item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath)
|
||||
item, err := GetOnePasswordItemByPath(ctx, h.opClient, OnePasswordItemPath)
|
||||
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
|
||||
}
|
||||
|
||||
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 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[ItemPathAnnotation] = itemPathString
|
||||
if err := h.client.Update(context.Background(), &secret); err != nil {
|
||||
log.Error(err, "failed to update secret %s annotations 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 annotations to version %s", secret.Name, itemVersion))
|
||||
continue
|
||||
}
|
||||
continue
|
||||
@@ -145,9 +164,11 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
|
||||
secret.Annotations[VersionAnnotation] = itemVersion
|
||||
secret.Annotations[ItemPathAnnotation] = itemPathString
|
||||
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]))
|
||||
if err := h.client.Update(context.Background(), &secret); err != nil {
|
||||
log.Error(err, "failed to update secret %s to version %d: %s", secret.Name, itemVersion, err)
|
||||
log.V(logs.DebugLevel).Info(fmt.Sprintf("New secret path: %v and version: %v",
|
||||
secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation],
|
||||
))
|
||||
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
|
||||
}
|
||||
if updatedSecrets[secret.Namespace] == nil {
|
||||
@@ -159,7 +180,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
|
||||
return updatedSecrets, nil
|
||||
}
|
||||
|
||||
func isItemLockedForForcedRestarts(item *onepassword.Item) bool {
|
||||
func isItemLockedForForcedRestarts(item *model.Item) bool {
|
||||
tags := item.Tags
|
||||
for i := 0; i < len(tags); i++ {
|
||||
if tags[i] == lockTag {
|
||||
@@ -171,15 +192,12 @@ func isItemLockedForForcedRestarts(item *onepassword.Item) bool {
|
||||
|
||||
func isUpdatedSecret(secretName string, updatedSecrets map[string]*corev1.Secret) bool {
|
||||
_, ok := updatedSecrets[secretName]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return ok
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string]bool, error) {
|
||||
func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap(ctx context.Context) (map[string]bool, error) {
|
||||
namespaces := &corev1.NamespaceList{}
|
||||
err := h.client.List(context.Background(), namespaces)
|
||||
err := h.client.List(ctx, namespaces)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to list kubernetes namespaces")
|
||||
return nil, err
|
||||
@@ -209,16 +227,22 @@ func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) s
|
||||
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]
|
||||
//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 == "" {
|
||||
return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace)
|
||||
}
|
||||
|
||||
restartDeploymentBool, err := utils.StringToBool(restartDeployment)
|
||||
if err != nil {
|
||||
log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secret.Name)
|
||||
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 restartDeploymentBool
|
||||
@@ -226,14 +250,17 @@ func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deploym
|
||||
|
||||
func isDeploymentSetForAutoRestart(deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
|
||||
restartDeployment := deployment.Annotations[RestartDeploymentsAnnotation]
|
||||
//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
|
||||
// If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
|
||||
if restartDeployment == "" {
|
||||
return setForAutoRestartByNamespace[deployment.Namespace]
|
||||
}
|
||||
|
||||
restartDeploymentBool, err := utils.StringToBool(restartDeployment)
|
||||
if err != nil {
|
||||
log.Error(err, "Error parsing %v annotation on Deployment %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, deployment.Name)
|
||||
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 restartDeploymentBool
|
||||
@@ -241,14 +268,16 @@ func isDeploymentSetForAutoRestart(deployment *appsv1.Deployment, setForAutoRest
|
||||
|
||||
func (h *SecretUpdateHandler) isNamespaceSetToAutoRestart(namespace *corev1.Namespace) bool {
|
||||
restartDeployment := namespace.Annotations[RestartDeploymentsAnnotation]
|
||||
//If annotation for auto restarts for deployment is not set. Check environment variable set on the operator
|
||||
// If annotation for auto restarts for deployment is not set. Check environment variable set on the operator
|
||||
if restartDeployment == "" {
|
||||
return h.shouldAutoRestartDeploymentsGlobal
|
||||
}
|
||||
|
||||
restartDeploymentBool, err := utils.StringToBool(restartDeployment)
|
||||
if err != nil {
|
||||
log.Error(err, "Error parsing %v annotation on Namespace %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, namespace.Name)
|
||||
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 restartDeploymentBool
|
||||
|
@@ -4,11 +4,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/onepassword/model"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/stretchr/testify/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
errors2 "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -40,7 +43,6 @@ type testUpdateSecretTask struct {
|
||||
existingSecret *corev1.Secret
|
||||
expectedError error
|
||||
expectedResultSecret *corev1.Secret
|
||||
expectedEvents []string
|
||||
opItem map[string]string
|
||||
expectedRestart bool
|
||||
globalAutoRestartEnabled bool
|
||||
@@ -60,6 +62,9 @@ var defaultNamespace = &corev1.Namespace{
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Refactor test cases to avoid duplication.
|
||||
//
|
||||
//nolint:dupl
|
||||
var tests = []testUpdateSecretTask{
|
||||
{
|
||||
testName: "Test unrelated deployment is not restarted with an updated secret",
|
||||
@@ -784,7 +789,7 @@ var tests = []testUpdateSecretTask{
|
||||
func TestUpdateSecretHandler(t *testing.T) {
|
||||
for _, testData := range tests {
|
||||
t.Run(testData.testName, func(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
// Register operator types with the runtime scheme.
|
||||
s := scheme.Scheme
|
||||
s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.existingDeployment)
|
||||
@@ -802,23 +807,15 @@ func TestUpdateSecretHandler(t *testing.T) {
|
||||
// Create a fake client to mock API calls.
|
||||
cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
|
||||
|
||||
opConnectClient := &mocks.TestClient{}
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
|
||||
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
|
||||
}
|
||||
mockOpClient := &mocks.TestClient{}
|
||||
mockOpClient.On("GetItemByID", mock.Anything, mock.Anything).Return(createItem(), nil)
|
||||
h := &SecretUpdateHandler{
|
||||
client: cl,
|
||||
opConnectClient: opConnectClient,
|
||||
opClient: mockOpClient,
|
||||
shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled,
|
||||
}
|
||||
|
||||
err := h.UpdateKubernetesSecretsTask()
|
||||
err := h.UpdateKubernetesSecretsTask(ctx)
|
||||
|
||||
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
|
||||
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 {
|
||||
assert.Error(t, err)
|
||||
@@ -843,9 +840,10 @@ func TestUpdateSecretHandler(t *testing.T) {
|
||||
assert.Equal(t, testData.expectedResultSecret.Annotations[VersionAnnotation], secret.Annotations[VersionAnnotation])
|
||||
}
|
||||
|
||||
//check if deployment has been restarted
|
||||
// check if deployment has been restarted
|
||||
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]
|
||||
if ok {
|
||||
@@ -854,7 +852,7 @@ func TestUpdateSecretHandler(t *testing.T) {
|
||||
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
|
||||
for name, expected := range oldPodTemplateAnnotations {
|
||||
actual, ok := newPodTemplateAnnotations[name]
|
||||
@@ -879,16 +877,23 @@ func TestIsUpdatedSecret(t *testing.T) {
|
||||
assert.True(t, isUpdatedSecret(secretName, updatedSecrets))
|
||||
}
|
||||
|
||||
func generateFields(username, password string) []*onepassword.ItemField {
|
||||
fields := []*onepassword.ItemField{
|
||||
{
|
||||
Label: "username",
|
||||
Value: username,
|
||||
},
|
||||
{
|
||||
Label: "password",
|
||||
Value: password,
|
||||
func createItem() *model.Item {
|
||||
return &model.Item{
|
||||
ID: itemId,
|
||||
VaultID: vaultId,
|
||||
Version: itemVersion,
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Fields: []model.ItemField{
|
||||
{
|
||||
Label: "username",
|
||||
Value: username,
|
||||
},
|
||||
{
|
||||
Label: "password",
|
||||
Value: password,
|
||||
},
|
||||
},
|
||||
Files: []model.File{},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
@@ -10,13 +10,14 @@ func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]*corev1.
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(volumes) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return len(volumes) > 0
|
||||
}
|
||||
|
||||
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++ {
|
||||
secret := IsVolumeUsingSecret(volumes[i], secrets)
|
||||
if secret != nil {
|
||||
|
8
pkg/testhelper/defaults/defaults.go
Normal file
8
pkg/testhelper/defaults/defaults.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package defaults
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
E2EInterval = 1 * time.Second
|
||||
E2ETimeout = 1 * time.Minute
|
||||
)
|
23
pkg/testhelper/kind/kind.go
Normal file
23
pkg/testhelper/kind/kind.go
Normal 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())
|
||||
}
|
125
pkg/testhelper/kube/deployment.go
Normal file
125
pkg/testhelper/kube/deployment.go
Normal 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 weren’t 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
224
pkg/testhelper/kube/kube.go
Normal 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 context’s 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())
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user