mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-23 07:58:04 +00:00
Compare commits
176 Commits
feature/mi
...
v1.9.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
8fc852a4dd | ||
![]() |
e6998497a2 | ||
![]() |
4b36cd80bd | ||
![]() |
030d451c94 | ||
![]() |
1e73bc1220 | ||
![]() |
a42a96bd26 | ||
![]() |
c8fe537ad1 | ||
![]() |
9b4d8eb292 | ||
![]() |
91c3422597 | ||
![]() |
d3d0cfa281 | ||
![]() |
5c41962aea | ||
![]() |
4413e61f2a | ||
![]() |
63e3cd15fb | ||
![]() |
ffb9a4f22a | ||
![]() |
10cfb55350 | ||
![]() |
372a5f4aa9 | ||
![]() |
3bb2f0e9d3 | ||
![]() |
a78b197db8 | ||
![]() |
514ef95330 | ||
![]() |
55922b52b7 | ||
![]() |
0c0a498726 | ||
![]() |
3d05fcc0d7 | ||
![]() |
4c9801322b | ||
![]() |
26ff2408ba | ||
![]() |
2dbfc7ecdd | ||
![]() |
aaddfd0c79 | ||
![]() |
e72705e9fa | ||
![]() |
c2d5c835c1 | ||
![]() |
e4b945ed56 | ||
![]() |
50862a8321 | ||
![]() |
c7548af5c3 | ||
![]() |
d00fc40e90 | ||
![]() |
802e7c5b56 | ||
![]() |
63dcaac407 | ||
![]() |
fe930fef05 | ||
![]() |
702974f750 | ||
![]() |
ea8773bfee | ||
![]() |
a84b5337ea | ||
![]() |
cd1c978d18 | ||
![]() |
34b8f9ee3d | ||
![]() |
03fa9adf6b | ||
![]() |
672396716d | ||
![]() |
08baab7218 | ||
![]() |
cb48c9c902 | ||
![]() |
b05c0661a0 | ||
![]() |
67330ceeed | ||
![]() |
24ac4fdc9e | ||
![]() |
710de1bbc0 | ||
![]() |
d99abbd432 | ||
![]() |
cc26230824 | ||
![]() |
30a1c136dc | ||
![]() |
245ec1bcec | ||
![]() |
c72174f743 | ||
![]() |
be3fdaa34e | ||
![]() |
ef40618af7 | ||
![]() |
5ced1c4e97 | ||
![]() |
fc1044aaab | ||
![]() |
c6c03ca157 | ||
![]() |
d75b029dfa | ||
![]() |
a132741778 | ||
![]() |
d8bfa318f2 | ||
![]() |
1d1d824ff4 | ||
![]() |
47922b05e2 | ||
![]() |
2712e9ce7b | ||
![]() |
20f81f5b0f | ||
![]() |
c8022336da | ||
![]() |
c4fdcc6f5f | ||
![]() |
916015cd75 | ||
![]() |
16d2101da8 | ||
![]() |
1a8bd75bc8 | ||
![]() |
256b1e09fd | ||
![]() |
11b1eae4e1 | ||
![]() |
108cdac29b | ||
![]() |
91eb658d70 | ||
![]() |
c3094dbef0 | ||
![]() |
e582d33b45 | ||
![]() |
87ff93daad | ||
![]() |
5e496d2e77 | ||
![]() |
1d75f78891 | ||
![]() |
23b66f73af | ||
![]() |
e8c380464b | ||
![]() |
f1da40aef7 | ||
![]() |
75501e5b1c | ||
![]() |
28c3ffade7 | ||
![]() |
946e986048 | ||
![]() |
250785f4af | ||
![]() |
e276ca1148 | ||
![]() |
be7b63c37e | ||
![]() |
622fcd64b8 | ||
![]() |
96b78795af | ||
![]() |
20b7a2c5cf | ||
![]() |
a3de05fbdb | ||
![]() |
a0460ce870 | ||
![]() |
1aa1a3f546 | ||
![]() |
69857c3d47 | ||
![]() |
ad276cb296 | ||
![]() |
eab5a4ad92 | ||
![]() |
128b9b2eb3 | ||
![]() |
867e699030 | ||
![]() |
ffab2cfdab | ||
![]() |
00436b4aee | ||
![]() |
0ca3415a47 | ||
![]() |
4aa1f7a669 | ||
![]() |
6c20db47d6 | ||
![]() |
874d5c57f9 | ||
![]() |
123cfa2c86 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -7,15 +7,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ^1.15
|
||||
go-version: ^1.21
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./... -cover
|
||||
run: make test
|
||||
|
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
|
25
.github/workflows/release-pr.yml
vendored
25
.github/workflows/release-pr.yml
vendored
@@ -14,9 +14,10 @@ jobs:
|
||||
outputs:
|
||||
result: ${{ steps.is_release_branch_without_pr.outputs.result }}
|
||||
steps:
|
||||
- id: is_release_branch_without_pr
|
||||
-
|
||||
id: is_release_branch_without_pr
|
||||
name: Find matching PR
|
||||
uses: actions/github-script@v3
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -27,7 +28,7 @@ jobs:
|
||||
|
||||
if(!releaseBranchName) { return false }
|
||||
|
||||
const {data: prs} = await github.pulls.list({
|
||||
const {data: prs} = await github.rest.pulls.list({
|
||||
...context.repo,
|
||||
state: 'open',
|
||||
head: `1Password:${releaseBranchName}`,
|
||||
@@ -42,19 +43,20 @@ jobs:
|
||||
name: Create Release Pull Request
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Parse release version
|
||||
id: get_version
|
||||
run: echo "::set-output name=version::$(echo $GITHUB_REF | sed 's|^refs/heads/release/v?*||g')"
|
||||
run: echo "version=$(echo "$GITHUB_REF" | sed 's|^refs/heads/release/v?*||g')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare Pull Request
|
||||
id: prep_pr
|
||||
run: |
|
||||
CHANGELOG_PATH=$(printf "%s/CHANGELOG.md" "${GITHUB_WORKSPACE}")
|
||||
|
||||
LOG_ENTRY=$(awk '/START\/v[0-9]+\.[0-9]+\.[0-9]+*/{f=1; next} /---/{if (f == 1) exit} f' "${CHANGELOG_PATH}")
|
||||
export PR_BODY=$(cat <<EOF
|
||||
DELIMITER="$(openssl rand -hex 8)" # DELIMITER is randomly generated and unique for each run. For more information, see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections.
|
||||
|
||||
PR_BODY_CONTENT="
|
||||
This is an automated PR for a new release.
|
||||
|
||||
Please check the following before approving:
|
||||
@@ -63,14 +65,9 @@ jobs:
|
||||
---
|
||||
## Release Changelog Preview
|
||||
${LOG_ENTRY}
|
||||
EOF
|
||||
)
|
||||
"
|
||||
|
||||
# Sanitizes multiline strings for action outputs (https://medium.com/agorapulse-stories/23f56447d209)
|
||||
PR_BODY="${PR_BODY//'%'/'%25'}"
|
||||
PR_BODY="${PR_BODY//$'\n'/'%0A'}"
|
||||
PR_BODY="${PR_BODY//$'\r'/'%0D'}"
|
||||
echo "::set-output name=pr_body::$(echo "$PR_BODY")"
|
||||
echo "pr_body<<${DELIMITER}${PR_BODY_CONTENT}${DELIMITER}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Create Pull Request via API
|
||||
id: post_pr
|
||||
|
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@@ -11,15 +11,14 @@ jobs:
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Docker meta
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: crazy-max/ghaction-docker-meta@v2
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
1password/onepassword-operator
|
||||
@@ -28,24 +27,25 @@ jobs:
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Get the version from tag
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Docker Login
|
||||
uses: docker/login-action@v1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
|
76
CHANGELOG.md
76
CHANGELOG.md
@@ -12,6 +12,82 @@
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.9.0)
|
||||
# v1.9.0
|
||||
|
||||
## Features
|
||||
* Enable the Operator to authenticate to 1Password using service accounts. {#160}
|
||||
|
||||
## Fixes
|
||||
* Update Operator to use SDK v1.34.1. {#185}
|
||||
* Pass Kubernetes context down to SDK/Connect. {#199}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.8.1)
|
||||
# v1.8.1
|
||||
|
||||
## Fixes
|
||||
* Upgrade operator to use Operator SDK v1.33.0. {#180}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.8.0)
|
||||
# v1.8.0
|
||||
|
||||
## Features
|
||||
* Added volume projected detection. Credit to @mmorejon. {#168}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.7.1)
|
||||
# v1.7.1
|
||||
|
||||
## Fixes
|
||||
* Adjusting logging level on various logs to reduce unnecessary logging. {#164}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.7.0)
|
||||
# v1.7.0
|
||||
|
||||
## Features
|
||||
* Upgraded operator to version 1.29.0. {#162}
|
||||
* Upgraded Golang version to 1.20. {#161}
|
||||
* Upgraded 1Password Connect version to 1.5.1. {#161}
|
||||
* Added runAsNonRoot and allowPrivalegeEscalation to specs. {#151}
|
||||
* Added code quality improvements. {#146}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.6.0)
|
||||
# v1.6.0
|
||||
|
||||
This version of the operator highlights the migration of the operator
|
||||
to use the latest version of the `operator-sdk` (`1.25.0` at the time of this release).
|
||||
|
||||
For the users, this shouldn't affect the functionality of the operator.
|
||||
|
||||
This migration enables us to use the new project structure, as well as updated packages that enables
|
||||
the team (as well as the contributors) to develop the operator more effective.
|
||||
|
||||
## Features
|
||||
* Migrate the operator to use the latest `operator-sdk` {#124}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.5.0)
|
||||
# v1.5.0
|
||||
|
||||
## Features
|
||||
* `OnePasswordItem` now contains a `status` which contains the status of creating the kubernetes secret for a OnePasswordItem. {#52}
|
||||
|
||||
## Fixes
|
||||
* The operator no longer logs an error about changing the secret type if the secret type is not actually being changed.
|
||||
* Annotations on a deployment are no longer removed when the operator triggers a restart. {#112}
|
||||
|
||||
---
|
||||
|
||||
[//]: # "START/v1.4.1"
|
||||
|
||||
# v1.4.1
|
||||
|
72
CONTRIBUTING.md
Normal file
72
CONTRIBUTING.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 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
|
||||
|
||||
- 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
|
||||
```
|
||||
|
||||
- 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)
|
25
Dockerfile
25
Dockerfile
@@ -1,21 +1,33 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.17 as builder
|
||||
FROM golang:1.24 as builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
# 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/
|
||||
|
||||
# Build
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
|
||||
# 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 \
|
||||
GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
|
||||
go build \
|
||||
-ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \
|
||||
-a -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
|
||||
@@ -23,5 +35,6 @@ FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/manager .
|
||||
USER 65532:65532
|
||||
COPY config/connect/ config/connect/
|
||||
|
||||
ENTRYPOINT ["/manager"]
|
||||
|
181
Makefile
181
Makefile
@@ -1,9 +1,11 @@
|
||||
export MAIN_BRANCH ?= main
|
||||
|
||||
# VERSION defines the project version for the bundle.
|
||||
# Update this value when you upgrade the version of your project.
|
||||
# To re-generate a bundle for another specific version without changing the standard setup, you can:
|
||||
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
|
||||
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
|
||||
VERSION ?= 0.0.1
|
||||
VERSION ?= 1.9.0
|
||||
|
||||
# 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")
|
||||
@@ -28,8 +30,8 @@ BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
|
||||
# This variable is used to construct full image tags for bundle and catalog images.
|
||||
#
|
||||
# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
|
||||
# onepassword.com/onepassword-operator-new-bundle:$VERSION and onepassword.com/onepassword-operator-new-catalog:$VERSION.
|
||||
IMAGE_TAG_BASE ?= onepassword.com/onepassword-operator-new
|
||||
# onepassword.com/onepassword-operator-bundle:$VERSION and onepassword.com/onepassword-operator-catalog:$VERSION.
|
||||
IMAGE_TAG_BASE ?= onepassword.com/onepassword-operator
|
||||
|
||||
# BUNDLE_IMG defines the image:tag used for the bundle.
|
||||
# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
|
||||
@@ -46,10 +48,14 @@ 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.34.1
|
||||
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= controller:latest
|
||||
IMG ?= 1password/onepassword-operator:latest
|
||||
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
||||
ENVTEST_K8S_VERSION = 1.23
|
||||
ENVTEST_K8S_VERSION = 1.29.1
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
@@ -58,8 +64,13 @@ 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.
|
||||
# This is a requirement for 'setup-envtest.sh' in the test target.
|
||||
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
|
||||
SHELL = /usr/bin/env bash -o pipefail
|
||||
.SHELLFLAGS = -ec
|
||||
@@ -104,25 +115,42 @@ vet: ## Run go vet against code.
|
||||
|
||||
.PHONY: test
|
||||
test: manifests generate fmt vet envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
|
||||
|
||||
##@ Build
|
||||
|
||||
.PHONY: build
|
||||
build: generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager main.go
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager cmd/main.go
|
||||
|
||||
.PHONY: run
|
||||
run: manifests generate fmt vet ## Run a controller from your host.
|
||||
go run ./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.
|
||||
# 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} .
|
||||
$(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
|
||||
# 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>> then the export will fail)
|
||||
# To properly provided solutions that supports more than one platform you should use this option.
|
||||
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
|
||||
.PHONY: docker-buildx
|
||||
docker-buildx: ## Build and push docker image for the manager for cross-platform support
|
||||
- $(CONTAINER_TOOL) buildx create --name project-v3-builder
|
||||
$(CONTAINER_TOOL) buildx use project-v3-builder
|
||||
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile .
|
||||
- $(CONTAINER_TOOL) buildx rm project-v3-builder
|
||||
|
||||
##@ Deployment
|
||||
|
||||
@@ -132,56 +160,85 @@ 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})
|
||||
|
||||
.PHONY: deploy
|
||||
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
|
||||
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 -
|
||||
$(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
|
||||
CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
|
||||
.PHONY: controller-gen
|
||||
controller-gen: ## Download controller-gen locally if necessary.
|
||||
$(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0)
|
||||
##@ Build Dependencies
|
||||
|
||||
## Location to install dependencies to
|
||||
LOCALBIN ?= $(shell pwd)/bin
|
||||
$(LOCALBIN):
|
||||
mkdir -p $(LOCALBIN)
|
||||
|
||||
## Tool Binaries
|
||||
KUBECTL ?= kubectl
|
||||
KUSTOMIZE ?= $(LOCALBIN)/kustomize
|
||||
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
|
||||
ENVTEST ?= $(LOCALBIN)/setup-envtest
|
||||
|
||||
## Tool Versions
|
||||
KUSTOMIZE_VERSION ?= v5.3.0
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.14.0
|
||||
|
||||
KUSTOMIZE = $(shell pwd)/bin/kustomize
|
||||
.PHONY: kustomize
|
||||
kustomize: ## Download kustomize locally if necessary.
|
||||
$(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7)
|
||||
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading.
|
||||
$(KUSTOMIZE): $(LOCALBIN)
|
||||
@if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \
|
||||
echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \
|
||||
rm -rf $(LOCALBIN)/kustomize; \
|
||||
fi
|
||||
test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION)
|
||||
|
||||
.PHONY: controller-gen
|
||||
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten.
|
||||
$(CONTROLLER_GEN): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \
|
||||
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
|
||||
|
||||
ENVTEST = $(shell pwd)/bin/setup-envtest
|
||||
.PHONY: envtest
|
||||
envtest: ## Download envtest-setup locally if necessary.
|
||||
$(call go-get-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)
|
||||
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
|
||||
$(ENVTEST): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
|
||||
|
||||
# go-get-tool will 'go get' any package $2 and install it to $1.
|
||||
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
define go-get-tool
|
||||
@[ -f $(1) ] || { \
|
||||
set -e ;\
|
||||
TMP_DIR=$$(mktemp -d) ;\
|
||||
cd $$TMP_DIR ;\
|
||||
go mod init tmp ;\
|
||||
echo "Downloading $(2)" ;\
|
||||
GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\
|
||||
rm -rf $$TMP_DIR ;\
|
||||
}
|
||||
endef
|
||||
.PHONY: 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.
|
||||
@@ -200,7 +257,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.19.1/$${OS}-$${ARCH}-opm ;\
|
||||
curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\
|
||||
chmod +x $(OPM) ;\
|
||||
}
|
||||
else
|
||||
@@ -231,3 +288,37 @@ catalog-build: opm ## Build a catalog image.
|
||||
.PHONY: catalog-push
|
||||
catalog-push: ## Push a catalog image.
|
||||
$(MAKE) docker-push IMG=$(CATALOG_IMG)
|
||||
|
||||
## Release functions =====================
|
||||
GIT_BRANCH := $(shell git symbolic-ref --short HEAD)
|
||||
WORKTREE_CLEAN := $(shell git status --porcelain 1>/dev/null 2>&1; echo $$?)
|
||||
SCRIPTS_DIR := $(CURDIR)/scripts
|
||||
|
||||
versionFile = $(CURDIR)/.VERSION
|
||||
curVersion := $(shell cat $(versionFile) | sed 's/^v//')
|
||||
|
||||
release/prepare: .check_git_clean ## Updates changelog and creates release branch (call with 'release/prepare version=<new_version_number>')
|
||||
|
||||
@test $(version) || (echo "[ERROR] version argument not set."; exit 1)
|
||||
@git fetch --quiet origin $(MAIN_BRANCH)
|
||||
|
||||
@echo $(version) | tr -d '\n' | tee $(versionFile) &>/dev/null
|
||||
|
||||
@NEW_VERSION=$(version) $(SCRIPTS_DIR)/prepare-release.sh
|
||||
|
||||
release/tag: .check_git_clean ## Creates git tag
|
||||
@git pull --ff-only
|
||||
@echo "Applying tag 'v$(curVersion)' to HEAD..."
|
||||
@git tag --sign "v$(curVersion)" -m "Release v$(curVersion)"
|
||||
@echo "[OK] Success!"
|
||||
@echo "Remember to call 'git push --tags' to persist the tag."
|
||||
|
||||
## Helper functions =====================
|
||||
|
||||
.check_git_clean:
|
||||
ifneq ($(GIT_BRANCH), $(MAIN_BRANCH))
|
||||
@echo "[ERROR] Please checkout default branch '$(MAIN_BRANCH)' and re-run this command."; exit 1;
|
||||
endif
|
||||
ifneq ($(WORKTREE_CLEAN), 0)
|
||||
@echo "[ERROR] Uncommitted changes found in worktree. Address them and try again."; exit 1;
|
||||
endif
|
||||
|
9
PROJECT
9
PROJECT
@@ -1,10 +1,14 @@
|
||||
# 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/v3
|
||||
- go.kubebuilder.io/v4
|
||||
plugins:
|
||||
manifests.sdk.operatorframework.io/v2: {}
|
||||
scorecard.sdk.operatorframework.io/v2: {}
|
||||
projectName: onepassword-operator-new
|
||||
projectName: onepassword-operator
|
||||
repo: github.com/1Password/onepassword-operator
|
||||
resources:
|
||||
- api:
|
||||
@@ -12,7 +16,6 @@ resources:
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: onepassword.com
|
||||
group: onepassword
|
||||
kind: OnePasswordItem
|
||||
path: github.com/1Password/onepassword-operator/api/v1
|
||||
version: v1
|
||||
|
240
README.md
240
README.md
@@ -1,241 +1,65 @@
|
||||
// TODO: Update README.md
|
||||
<!-- Image sourced from https://blog.1password.com/introducing-secrets-automation/ -->
|
||||
<img alt="" role="img" src="https://blog.1password.com/posts/2021/secrets-automation-launch/header.svg"/>
|
||||
|
||||
# 1Password Connect Kubernetes Operator
|
||||
<div align="center">
|
||||
<h1>1Password Connect Kubernetes Operator</h1>
|
||||
<p>Integrate <a href="https://developer.1password.com/docs/connect">1Password Connect</a> with your Kubernetes Infrastructure</p>
|
||||
<a href="https://github.com/1Password/onepassword-operator#-get-started">
|
||||
<img alt="Get started" src="https://user-images.githubusercontent.com/45081667/226940040-16d3684b-60f4-4d95-adb2-5757a8f1bc15.png" height="37"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item.
|
||||
---
|
||||
|
||||
The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment.
|
||||
The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes Secrets with 1Password. The operator also handles autorestarting deployments when 1Password items are updated.
|
||||
|
||||
The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted.
|
||||
## ✨ Get started
|
||||
|
||||
## Setup
|
||||
### 🚀 Quickstart
|
||||
|
||||
Prerequisites:
|
||||
1. Add the [1Password Helm Chart](https://github.com/1Password/connect-helm-charts) to your repository.
|
||||
|
||||
- [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/)
|
||||
- [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
- [docker installed](https://docs.docker.com/get-docker/)
|
||||
- [Generated a 1password-credentials.json file and issued a 1Password Connect API Token for the K8s Operator integration](https://support.1password.com/secrets-automation/)
|
||||
- [1Password Connect deployed to Kubernetes](https://support.1password.com/connect-deploy-kubernetes/#step-2-deploy-a-1password-connect-server). **NOTE**: If customization of the 1Password Connect deployment is not required you can skip this prerequisite.
|
||||
2. Run the following command to install Connect and the 1Password Kubernetes Operator in your infrastructure:
|
||||
|
||||
### Quickstart for Deploying 1Password Connect to Kubernetes
|
||||
|
||||
|
||||
#### Deploy with Helm
|
||||
The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes.
|
||||
|
||||
[The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts)
|
||||
|
||||
#### Deploy using the Connect Operator
|
||||
If 1Password Connect is already running, you can skip this step. This guide will provide a quickstart option for deploying a default configuration of 1Password Connect via starting the deploying the 1Password Connect Operator, however it is recommended that you instead deploy your own manifest file if customization of the 1Password Connect deployment is desired.
|
||||
|
||||
Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session:
|
||||
|
||||
```bash
|
||||
cat 1password-credentials.json | base64 | \
|
||||
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
|
||||
```
|
||||
helm install connect 1password/connect --set-file connect.credentials=1password-credentials-demo.json --set operator.create=true --set operator.token.value = <your connect token>
|
||||
```
|
||||
|
||||
Create a Kubernetes secret from the op-session file:
|
||||
```bash
|
||||
kubectl create secret generic op-credentials --from-file=op-session
|
||||
3. Create a Kubernetes Secret from a 1Password item:
|
||||
|
||||
```
|
||||
|
||||
Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`:
|
||||
```yaml
|
||||
- name: MANAGE_CONNECT
|
||||
value: "true"
|
||||
```
|
||||
Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the `default` namespace.
|
||||
### Kubernetes Operator Deployment
|
||||
|
||||
**Create Kubernetes Secret for OP_CONNECT_TOKEN**
|
||||
|
||||
"Create a Connect token for the operator and save it as a Kubernetes Secret:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>"
|
||||
```
|
||||
|
||||
If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command:
|
||||
```bash
|
||||
kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
|
||||
```
|
||||
|
||||
[More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens)
|
||||
|
||||
**Set Permissions For Operator**
|
||||
|
||||
We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder.
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/permissions.yaml
|
||||
```
|
||||
|
||||
**Create Custom One Password Secret Resource**
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
|
||||
```
|
||||
|
||||
**Deploying the Operator**
|
||||
|
||||
An sample Deployment yaml can be found at `/deploy/operator.yaml`.
|
||||
|
||||
|
||||
To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml:
|
||||
|
||||
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect.
|
||||
- **WATCH_NAMESPACE:** (default: watch all namespaces): Comma separated list of what Namespaces to watch for changes.
|
||||
- **POLLING_INTERVAL** (default: 600): The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
|
||||
- **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace.
|
||||
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
|
||||
|
||||
Apply the deployment file:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f deploy/operator.yaml
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To create a Kubernetes Secret from a 1Password item, create a yaml file with the following
|
||||
|
||||
```yaml
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
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>"
|
||||
itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
|
||||
```
|
||||
|
||||
Deploy the OnePasswordItem to Kubernetes:
|
||||
|
||||
```bash
|
||||
```
|
||||
kubectl apply -f <your_item>.yaml
|
||||
```
|
||||
|
||||
To test that the Kubernetes Secret check that the following command returns a secret:
|
||||
Check that the Kubernetes Secret has been generated:
|
||||
|
||||
```bash
|
||||
```
|
||||
kubectl get secret <secret_name>
|
||||
```
|
||||
|
||||
Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.
|
||||
### 📄 Usage
|
||||
|
||||
To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata:
|
||||
Refer to the [Usage Guide](USAGEGUIDE.md) for documentation on how to deploy and use the 1Password Operator.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: deployment-example
|
||||
annotations:
|
||||
operator.1password.io/item-path: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
|
||||
operator.1password.io/item-name: "<secret_name>"
|
||||
```
|
||||
## 💙 Community & Support
|
||||
|
||||
Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Path.
|
||||
- File an [issue](https://github.com/1Password/onepassword-operator/issues) for bugs and feature requests.
|
||||
- Join the [Developer Slack workspace](https://join.slack.com/t/1password-devs/shared_invite/zt-1halo11ps-6o9pEv96xZ3LtX_VE0fJQA).
|
||||
- Subscribe to the [Developer Newsletter](https://1password.com/dev-subscribe/).
|
||||
|
||||
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.
|
||||
## 🔐 Security
|
||||
|
||||
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.
|
||||
1Password requests you practice responsible disclosure if you discover a vulnerability.
|
||||
|
||||
Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret.
|
||||
|
||||
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
|
||||
|
||||
---
|
||||
**NOTE**
|
||||
|
||||
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.
|
||||
|
||||
Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format:
|
||||
- Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed
|
||||
- All whitespaces between words will be replaced by `-`
|
||||
- All the letters will be lower-cased.
|
||||
|
||||
---
|
||||
|
||||
### Configuring Automatic Rolling Restarts of Deployments
|
||||
|
||||
If a 1Password Item that is linked to a Kubernetes Secret is updated, any deployments configured to `auto-restart` AND are using that secret will be given a rolling restart the next time 1Password Connect is polled for updates.
|
||||
|
||||
There are many levels of granularity on which to configure auto restarts on deployments: at the operator level, per-namespace, or per-deployment.
|
||||
|
||||
**On the operator**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environemnt variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false.
|
||||
|
||||
**Per Namespace**: This method allows for managing auto restarts on all deployments within a namespace. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired namespace. An example of this is shown below:
|
||||
```yaml
|
||||
# enabled auto restarts for all deployments within a namespace unless overwritten within a deployment
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "example-namespace"
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment.
|
||||
|
||||
**Per Deployment**
|
||||
This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below:
|
||||
```yaml
|
||||
# enabled auto restarts for the deployment
|
||||
apiVersion: v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "example-deployment"
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
If the value is not set, the auto restart settings on the namespace will be used.
|
||||
|
||||
**Per OnePasswordItem Custom Resource**
|
||||
This method allows for managing auto restarts on a given OnePasswordItem custom resource. Auto restarts can by managed by setting the annotation `operator.1password.io/auto_restart` to either `true` or `false` on the desired OnePasswordItem. An example of this is shown below:
|
||||
```yaml
|
||||
# enabled auto restarts for the OnePasswordItem
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: example
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
If the value is not set, the auto restart settings on the deployment will be used.
|
||||
|
||||
## Development
|
||||
|
||||
### Creating a Docker image
|
||||
|
||||
To create a local version of the Docker image for testing, use the following `Makefile` target:
|
||||
```shell
|
||||
make build/local
|
||||
```
|
||||
|
||||
### Building the Operator binary
|
||||
```shell
|
||||
make build/binary
|
||||
```
|
||||
|
||||
The binary will be placed inside a `dist` folder within this repository.
|
||||
|
||||
### Running Tests
|
||||
|
||||
```shell
|
||||
make test
|
||||
```
|
||||
|
||||
With coverage:
|
||||
```shell
|
||||
make test/coverage
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
1Password requests you practice responsible disclosure if you discover a vulnerability.
|
||||
|
||||
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
|
||||
|
||||
For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits).
|
||||
Please file requests by sending an email to bugbounty@agilebits.com.
|
||||
|
221
USAGEGUIDE.md
Normal file
221
USAGEGUIDE.md
Normal file
@@ -0,0 +1,221 @@
|
||||
<img alt="" role="img" src="https://blog.1password.com/posts/2021/secrets-automation-launch/header.svg"/>
|
||||
<div align="center">
|
||||
<h1>Usage Guide</h1>
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Configuration Options](#configuration-options)
|
||||
2. [Use Kubernetes Operator with Service Account](#use-kubernetes-operator-with-service-account)
|
||||
- [Create a Service Account](#1-create-a-service-account)
|
||||
- [Create a Kubernetes secret](#2-create-a-kubernetes-secret-for-the-service-account)
|
||||
- [Deploy the Operator](#3-deploy-the-operator)
|
||||
3. [Use Kubernetes Operator with Connect](#use-kubernetes-operator-with-connect)
|
||||
- [Deploy with Helm](#1-deploy-with-helm)
|
||||
- [Deploy manually](#2-deploy-manually)
|
||||
4. [Logging level](#logging-level)
|
||||
5. [Usage examples](#usage-examples)
|
||||
6. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets)
|
||||
7. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
|
||||
8. [Development](#development)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Configuration options
|
||||
There are 2 ways 1Password Operator can talk to 1Password servers:
|
||||
- [1Password Service Accounts](https://developer.1password.com/docs/service-accounts)
|
||||
- [1Password Connect](https://developer.1password.com/docs/connect/)
|
||||
|
||||
---
|
||||
|
||||
## Use Kubernetes Operator with Service Account
|
||||
|
||||
### 1. [Create a service account](https://developer.1password.com/docs/service-accounts/get-started#create-a-service-account)
|
||||
### 2. Create a Kubernetes secret for the Service Account
|
||||
- Set `OP_SERVICE_ACCOUNT_TOKEN` environment variable to the service account token you created in the previous step. This token will be used by the operator to access 1Password items.
|
||||
- Create Kubernetes secret:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic onepassword-service-account-token --from-literal=token="$OP_SERVICE_ACCOUNT_TOKEN"
|
||||
```
|
||||
|
||||
### 3. Deploy the Operator
|
||||
|
||||
An sample Deployment yaml can be found at `/config/manager/manager.yaml`.
|
||||
To use Operator with Service Account, you need to set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable in the `/config/manager/manager.yaml`. And remove `OP_CONNECT_TOKEN` and `OP_CONNECT_HOST` environment variables.
|
||||
|
||||
To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml:
|
||||
|
||||
- **OP_SERVICE_ACCOUNT_TOKEN** *(required)*: Specifies Service Account token within Kubernetes to access the 1Password items.
|
||||
- **WATCH_NAMESPACE:** *(default: watch all namespaces)*: Comma separated list of what Namespaces to watch for changes.
|
||||
- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password.
|
||||
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
|
||||
|
||||
To deploy the operator, simply run the following command:
|
||||
|
||||
```shell
|
||||
make deploy
|
||||
```
|
||||
|
||||
**Undeploy Operator**
|
||||
|
||||
```
|
||||
make undeploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Kubernetes Operator with Connect
|
||||
|
||||
### 1. [Deploy with Helm](https://developer.1password.com/docs/k8s/operator/?deployment-type=helm#helm-step-1)
|
||||
### 2. [Deploy manually](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#manual-step-1)
|
||||
|
||||
To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml:
|
||||
|
||||
- **OP_CONNECT_HOST** *(required)*: Specifies the host name within Kubernetes in which to access the 1Password Connect.
|
||||
- **WATCH_NAMESPACE:** *(default: watch all namespaces)*: Comma separated list of what Namespaces to watch for changes.
|
||||
- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
|
||||
- **MANAGE_CONNECT** *(default: false)*: If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the current namespace.
|
||||
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
|
||||
|
||||
---
|
||||
|
||||
## Logging level
|
||||
You can set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. The default value is `debug`.
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
....
|
||||
containers:
|
||||
- command:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
- --zap-log-level=info
|
||||
image: 1password/onepassword-operator:latest
|
||||
....
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage examples
|
||||
Find usage [examples](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#usage-examples) on 1Password developer documentation.
|
||||
|
||||
---
|
||||
|
||||
## How 1Password Items Map to Kubernetes Secrets
|
||||
|
||||
The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password.
|
||||
In case of fields that store files, the file's contents will be used as the value.
|
||||
|
||||
Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence.
|
||||
|
||||
Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret.
|
||||
|
||||
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
|
||||
|
||||
|
||||
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.
|
||||
|
||||
Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format:
|
||||
|
||||
- Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed
|
||||
- All whitespaces between words will be replaced by `-`
|
||||
- All the letters will be lower-cased.
|
||||
|
||||
---
|
||||
|
||||
## Configuring Automatic Rolling Restarts of Deployments
|
||||
|
||||
If a 1Password Item that is linked to a Kubernetes Secret is updated, any deployments configured to `auto-restart` AND are using that secret will be given a rolling restart the next time 1Password Connect is polled for updates.
|
||||
|
||||
There are many levels of granularity on which to configure auto restarts on deployments:
|
||||
- Operator level
|
||||
- Per-namespace
|
||||
- Per-deployment
|
||||
|
||||
**Operator Level**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environment variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false.
|
||||
|
||||
**Per Namespace**: This method allows for managing auto restarts on all deployments within a namespace. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired namespace. An example of this is shown below:
|
||||
|
||||
```yaml
|
||||
# enabled auto restarts for all deployments within a namespace unless overwritten within a deployment
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "example-namespace"
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
|
||||
If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment.
|
||||
|
||||
**Per Deployment**
|
||||
This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below:
|
||||
|
||||
```yaml
|
||||
# enabled auto restarts for the deployment
|
||||
apiVersion: v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "example-deployment"
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
|
||||
If the value is not set, the auto restart settings on the namespace will be used.
|
||||
|
||||
**Per OnePasswordItem Custom Resource**
|
||||
This method allows for managing auto restarts on a given OnePasswordItem custom resource. Auto restarts can by managed by setting the annotation `operator.1password.io/auto_restart` to either `true` or `false` on the desired OnePasswordItem. An example of this is shown below:
|
||||
|
||||
```yaml
|
||||
# enabled auto restarts for the OnePasswordItem
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: example
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
|
||||
If the value is not set, the auto restart settings on the deployment will be used.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### How it works
|
||||
|
||||
This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
|
||||
|
||||
It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/)
|
||||
which provides a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster
|
||||
|
||||
### Test It Out
|
||||
|
||||
1. Install the CRDs into the cluster:
|
||||
|
||||
```sh
|
||||
make install
|
||||
```
|
||||
|
||||
2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running):
|
||||
|
||||
```sh
|
||||
make run
|
||||
```
|
||||
|
||||
**NOTE:** You can also run this in one step by running: `make install run`
|
||||
|
||||
### Modifying the API definitions
|
||||
|
||||
If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
|
||||
|
||||
```sh
|
||||
make manifests
|
||||
```
|
||||
|
||||
**NOTE:** Run `make --help` for more information on all potential `make` targets
|
||||
|
||||
More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
|
@@ -1,22 +1,30 @@
|
||||
/*
|
||||
Copyright 2022.
|
||||
MIT License
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
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:
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
// Package v1 contains API Schema definitions for the onepassword v1 API group
|
||||
//+kubebuilder:object:generate=true
|
||||
//+groupName=onepassword.onepassword.com
|
||||
// Package v1 contains API Schema definitions for the v1 API group
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=onepassword.com
|
||||
package v1
|
||||
|
||||
import (
|
||||
@@ -26,7 +34,7 @@ import (
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
GroupVersion = schema.GroupVersion{Group: "onepassword.onepassword.com", Version: "v1"}
|
||||
GroupVersion = schema.GroupVersion{Group: "onepassword.com", Version: "v1"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||
|
@@ -1,17 +1,25 @@
|
||||
/*
|
||||
Copyright 2022.
|
||||
MIT License
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
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:
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package v1
|
||||
@@ -28,14 +36,35 @@ type OnePasswordItemSpec struct {
|
||||
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
|
||||
// Foo is an example field of OnePasswordItem. Edit onepassworditem_types.go to remove/update
|
||||
ItemPath string `json:"itemPath,omitempty"`
|
||||
}
|
||||
|
||||
type OnePasswordItemConditionType string
|
||||
|
||||
const (
|
||||
// OnePasswordItemReady means the Kubernetes secret is ready for use.
|
||||
OnePasswordItemReady OnePasswordItemConditionType = "Ready"
|
||||
)
|
||||
|
||||
type OnePasswordItemCondition struct {
|
||||
// Type of job condition, Completed.
|
||||
Type OnePasswordItemConditionType `json:"type"`
|
||||
// Status of the condition, one of True, False, Unknown.
|
||||
Status metav1.ConditionStatus `json:"status"`
|
||||
// Last time the condition transit from one status to another.
|
||||
// +optional
|
||||
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
|
||||
// Human-readable message indicating details about last transition.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// OnePasswordItemStatus defines the observed state of OnePasswordItem
|
||||
type OnePasswordItemStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
|
||||
Conditions []OnePasswordItemCondition `json:"conditions"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
@@ -45,8 +74,9 @@ type OnePasswordItemStatus struct {
|
||||
type OnePasswordItem struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
|
||||
// Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
|
||||
Type string `json:"type,omitempty"`
|
||||
Spec OnePasswordItemSpec `json:"spec,omitempty"`
|
||||
Status OnePasswordItemStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
@@ -1,20 +1,27 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2022.
|
||||
MIT License
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
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:
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
@@ -31,7 +38,7 @@ func (in *OnePasswordItem) DeepCopyInto(out *OnePasswordItem) {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
out.Status = in.Status
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem.
|
||||
@@ -52,6 +59,22 @@ func (in *OnePasswordItem) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OnePasswordItemCondition) DeepCopyInto(out *OnePasswordItemCondition) {
|
||||
*out = *in
|
||||
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemCondition.
|
||||
func (in *OnePasswordItemCondition) DeepCopy() *OnePasswordItemCondition {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OnePasswordItemCondition)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
|
||||
*out = *in
|
||||
@@ -102,6 +125,13 @@ func (in *OnePasswordItemSpec) DeepCopy() *OnePasswordItemSpec {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]OnePasswordItemCondition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus.
|
||||
|
@@ -1,40 +1,41 @@
|
||||
/*
|
||||
Copyright 2022.
|
||||
MIT License
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
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:
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
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"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
"github.com/1Password/onepassword-operator/version"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
||||
|
||||
// sdkVersion "github.com/operator-framework/operator-sdk/version"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
@@ -42,36 +43,57 @@ import (
|
||||
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/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
"github.com/1Password/onepassword-operator/controllers"
|
||||
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")
|
||||
WatchNamespaceEnvVar = "WATCH_NAMESPACE"
|
||||
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(onepasswordv1.AddToScheme(scheme))
|
||||
utilruntime.Must(onepasswordcomv1.AddToScheme(scheme))
|
||||
//+kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
setupLog.Info(fmt.Sprintf("Operator Version: %s", version.Version))
|
||||
setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
|
||||
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
|
||||
// TODO figure out how to get operator-sdk version
|
||||
// setupLog.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version))
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
@@ -91,24 +113,40 @@ func main() {
|
||||
|
||||
printVersion()
|
||||
|
||||
namespace := os.Getenv(WatchNamespaceEnvVar)
|
||||
// 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)
|
||||
}
|
||||
|
||||
options := ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Namespace: namespace,
|
||||
MetricsBindAddress: metricsAddr,
|
||||
Port: 9443,
|
||||
Metrics: metricsserver.Options{BindAddress: metricsAddr},
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "c26807fd.onepassword.com",
|
||||
}
|
||||
|
||||
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
|
||||
// Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate
|
||||
// Also note that you may face performance issues when using this with a high number of namespaces.
|
||||
if strings.Contains(namespace, ",") {
|
||||
options.Namespace = ""
|
||||
options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(namespace, ","))
|
||||
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)
|
||||
@@ -118,22 +156,76 @@ func main() {
|
||||
}
|
||||
|
||||
// Setup One Password Client
|
||||
opConnectClient, err := connect.NewClientFromEnvironment()
|
||||
opClient, err := opclient.NewFromEnvironment(ctx, opclient.Config{
|
||||
Logger: setupLog,
|
||||
Version: version.OperatorVersion,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "failed to create 1Password client")
|
||||
setupLog.Error(err, "unable to create 1Password client")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controllers.OnePasswordItemReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
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 == false {
|
||||
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 err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
@@ -144,69 +236,25 @@ func main() {
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
if err := mgr.Start(ctx); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
deploymentNamespace, err := utils.GetOperatorNamespace()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "Failed to get namespace")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
//Setup 1PasswordConnect
|
||||
if shouldManageConnect() {
|
||||
setupLog.Info("Automated Connect Management Enabled")
|
||||
go func() {
|
||||
connectStarted := false
|
||||
for !connectStarted {
|
||||
err := op.SetupConnect(mgr.GetClient(), deploymentNamespace)
|
||||
// Cache Not Started is an acceptable error. Retry until cache is started.
|
||||
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err == nil {
|
||||
connectStarted = true
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
setupLog.Info("Automated Connect Management Disabled")
|
||||
}
|
||||
|
||||
// TODO: Configure Metrics Service. See: https://sdk.operatorframework.io/docs/building-operators/golang/migration/#export-metrics
|
||||
|
||||
// Setup update secrets task
|
||||
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments())
|
||||
done := make(chan bool)
|
||||
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := updatedSecretsPoller.UpdateKubernetesSecretsTask()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "error running update kubernetes secret task")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the Cmd
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "Manager exited non-zero")
|
||||
done <- true
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const manageConnect = "MANAGE_CONNECT"
|
||||
// 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)
|
||||
@@ -221,8 +269,18 @@ func shouldManageConnect() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
const envPollingIntervalVariable = "POLLING_INTERVAL"
|
||||
const defaultPollingInterval = 600
|
||||
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)
|
||||
@@ -237,18 +295,3 @@ func getPollingIntervalForUpdatingSecrets() time.Duration {
|
||||
setupLog.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval))
|
||||
return time.Duration(defaultPollingInterval) * time.Second
|
||||
}
|
||||
|
||||
const restartDeploymentsEnvVariable = "AUTO_RESTART"
|
||||
|
||||
func shouldAutoRestartDeployments() bool {
|
||||
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
|
||||
if found {
|
||||
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldAutoRestartDeploymentsBool
|
||||
}
|
||||
return false
|
||||
}
|
@@ -12,6 +12,8 @@ spec:
|
||||
app: onepassword-connect
|
||||
version: "1.0.0"
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
volumes:
|
||||
- name: shared-data
|
||||
emptyDir: {}
|
||||
@@ -32,9 +34,12 @@ spec:
|
||||
containers:
|
||||
- name: connect-api
|
||||
image: 1password/connect-api:latest
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
requests:
|
||||
cpu: "0.2"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -49,9 +54,12 @@ spec:
|
||||
name: shared-data
|
||||
- name: connect-sync
|
||||
image: 1password/connect-sync:latest
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
requests:
|
||||
cpu: "0.2"
|
||||
ports:
|
||||
- containerPort: 8081
|
@@ -9,7 +9,7 @@ spec:
|
||||
ports:
|
||||
- port: 8080
|
||||
name: connect-api
|
||||
nodePort: 31080
|
||||
nodePort: 30080
|
||||
- port: 8081
|
||||
name: connect-sync
|
||||
nodePort: 31081
|
||||
nodePort: 30081
|
81
config/crd/bases/onepassword.com_onepassworditems.yaml
Normal file
81
config/crd/bases/onepassword.com_onepassworditems.yaml
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
name: onepassworditems.onepassword.com
|
||||
spec:
|
||||
group: onepassword.com
|
||||
names:
|
||||
kind: OnePasswordItem
|
||||
listKind: OnePasswordItemList
|
||||
plural: onepassworditems
|
||||
singular: onepassworditem
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: OnePasswordItem is the Schema for the onepassworditems API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: OnePasswordItemSpec defines the desired state of OnePasswordItem
|
||||
properties:
|
||||
itemPath:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: OnePasswordItemStatus defines the observed state of OnePasswordItem
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: Last time the condition transit from one status
|
||||
to another.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: Human-readable message indicating details about
|
||||
last transition.
|
||||
type: string
|
||||
status:
|
||||
description: Status of the condition, one of True, False, Unknown.
|
||||
type: string
|
||||
type:
|
||||
description: Type of job condition, Completed.
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- conditions
|
||||
type: object
|
||||
type:
|
||||
description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
|
||||
type: string
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
@@ -1,45 +0,0 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: onepassworditems.onepassword.com
|
||||
spec:
|
||||
group: onepassword.com
|
||||
names:
|
||||
kind: OnePasswordItem
|
||||
listKind: OnePasswordItemList
|
||||
plural: onepassworditems
|
||||
singular: onepassworditem
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: OnePasswordItem is the Schema for the onepassworditems API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: OnePasswordItemSpec defines the desired state of OnePasswordItem
|
||||
properties:
|
||||
itemPath:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: OnePasswordItemStatus defines the observed state of OnePasswordItem
|
||||
type: object
|
||||
type:
|
||||
description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
|
||||
type: string
|
||||
type: object
|
@@ -1,6 +0,0 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: example
|
||||
spec:
|
||||
itemPath: "vaults/<vault_id>/items/<item_id>"
|
@@ -2,20 +2,22 @@
|
||||
# since it depends on service name and namespace that are out of this kustomize package.
|
||||
# It should be run by config/default
|
||||
resources:
|
||||
- bases/onepassword.onepassword.com_onepassworditems.yaml
|
||||
- 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
|
||||
#- path: 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,39 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
name: onepassword-connect-operator
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
spec:
|
||||
serviceAccountName: onepassword-connect-operator
|
||||
containers:
|
||||
- name: onepassword-connect-operator
|
||||
image: 1password/onepassword-operator
|
||||
command: ["/manager"]
|
||||
env:
|
||||
- name: WATCH_NAMESPACE
|
||||
value: "default"
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: OPERATOR_NAME
|
||||
value: "onepassword-connect-operator"
|
||||
- name: OP_CONNECT_HOST
|
||||
value: "http://onepassword-connect:8080"
|
||||
- name: POLLING_INTERVAL
|
||||
value: "10"
|
||||
- name: OP_CONNECT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: onepassword-token
|
||||
key: token
|
||||
- name: AUTO_RESTART
|
||||
value: "false"
|
@@ -1,39 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
name: onepassword-connect-operator
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
spec:
|
||||
serviceAccountName: onepassword-connect-operator
|
||||
containers:
|
||||
- name: onepassword-connect-operator
|
||||
image: 1password/onepassword-operator
|
||||
command: ["/manager"]
|
||||
env:
|
||||
- name: WATCH_NAMESPACE
|
||||
value: "default,development"
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: OPERATOR_NAME
|
||||
value: "onepassword-connect-operator"
|
||||
- name: OP_CONNECT_HOST
|
||||
value: "http://onepassword-connect:8080"
|
||||
- name: POLLING_INTERVAL
|
||||
value: "10"
|
||||
- name: OP_CONNECT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: onepassword-token
|
||||
key: token
|
||||
- name: AUTO_RESTART
|
||||
value: "false"
|
@@ -3,5 +3,5 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
|
||||
name: onepassworditems.onepassword.onepassword.com
|
||||
cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
|
||||
name: onepassworditems.onepassword.com
|
||||
|
@@ -2,7 +2,7 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: onepassworditems.onepassword.onepassword.com
|
||||
name: onepassworditems.onepassword.com
|
||||
spec:
|
||||
conversion:
|
||||
strategy: Webhook
|
||||
|
@@ -1,114 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: onepassword-connect-operator-default
|
||||
namespace: default
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: onepassword-connect-operator
|
||||
namespace: default
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: onepassword-connect-operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: onepassword-connect-operator-development
|
||||
namespace: development
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: onepassword-connect-operator
|
||||
namespace: default
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: onepassword-connect-operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: onepassword-connect-operator
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
- services
|
||||
- services/finalizers
|
||||
- endpoints
|
||||
- persistentvolumeclaims
|
||||
- events
|
||||
- configmaps
|
||||
- secrets
|
||||
- namespaces
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
- daemonsets
|
||||
- replicasets
|
||||
- statefulsets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- monitoring.coreos.com
|
||||
resources:
|
||||
- servicemonitors
|
||||
verbs:
|
||||
- get
|
||||
- create
|
||||
- apiGroups:
|
||||
- apps
|
||||
resourceNames:
|
||||
- onepassword-connect-operator
|
||||
resources:
|
||||
- deployments/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- replicasets
|
||||
- deployments
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
@@ -1,18 +1,20 @@
|
||||
# Adds namespace to all resources.
|
||||
namespace: onepassword-operator-new-system
|
||||
# namespace: onepassword-connect-operator
|
||||
|
||||
# Value of this field is prepended to the
|
||||
# names of all resources, e.g. a deployment named
|
||||
# "wordpress" becomes "alices-wordpress".
|
||||
# Note that it should also match with the prefix (text before '-') of the namespace
|
||||
# field above.
|
||||
namePrefix: onepassword-operator-new-
|
||||
# namePrefix: onepassword-connect-
|
||||
|
||||
# Labels to add to all resources and selectors.
|
||||
#commonLabels:
|
||||
# someName: someValue
|
||||
#labels:
|
||||
#- includeSelectors: true
|
||||
# pairs:
|
||||
# someName: someValue
|
||||
|
||||
bases:
|
||||
resources:
|
||||
- ../crd
|
||||
- ../rbac
|
||||
- ../manager
|
||||
@@ -24,51 +26,117 @@ bases:
|
||||
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||
#- ../prometheus
|
||||
|
||||
patchesStrategicMerge:
|
||||
patches:
|
||||
# Protect the /metrics endpoint by putting it behind auth.
|
||||
# If you want your controller-manager to expose the /metrics
|
||||
# endpoint w/o any authn/z, please comment the following line.
|
||||
- manager_auth_proxy_patch.yaml
|
||||
|
||||
# Mount the controller config file for loading manager configurations
|
||||
# through a ComponentConfig type
|
||||
#- manager_config_patch.yaml
|
||||
- path: manager_auth_proxy_patch.yaml
|
||||
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- manager_webhook_patch.yaml
|
||||
#- path: 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: webhookcainjection_patch.yaml
|
||||
|
||||
# the following config is for teaching kustomize how to do var substitution
|
||||
vars:
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
||||
#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
|
||||
# objref:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # this name should match the one in certificate.yaml
|
||||
# fieldref:
|
||||
# fieldpath: metadata.namespace
|
||||
#- name: CERTIFICATE_NAME
|
||||
# objref:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # this name should match the one in certificate.yaml
|
||||
#- name: SERVICE_NAMESPACE # namespace of the service
|
||||
# objref:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldref:
|
||||
# fieldpath: metadata.namespace
|
||||
#- name: SERVICE_NAME
|
||||
# objref:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# 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
|
||||
|
@@ -3,14 +3,21 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
containers:
|
||||
- name: kube-rbac-proxy
|
||||
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0
|
||||
args:
|
||||
- "--secure-listen-address=0.0.0.0:8443"
|
||||
- "--upstream=http://127.0.0.1:8080/"
|
||||
|
@@ -1,20 +1,10 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: manager
|
||||
args:
|
||||
- "--config=controller_manager_config.yaml"
|
||||
volumeMounts:
|
||||
- name: manager-config
|
||||
mountPath: /controller_manager_config.yaml
|
||||
subPath: controller_manager_config.yaml
|
||||
volumes:
|
||||
- name: manager-config
|
||||
configMap:
|
||||
name: manager-config
|
||||
|
@@ -1,11 +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
|
@@ -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
|
||||
|
@@ -2,39 +2,111 @@ apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
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: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
app.kubernetes.io/name: deployment
|
||||
app.kubernetes.io/instance: controller-manager
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/default-container: manager
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
spec:
|
||||
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
|
||||
# according to the platforms which are supported by your solution.
|
||||
# It is considered best practice to support multiple architectures. You can
|
||||
# build your manager image using the makefile target docker-buildx.
|
||||
# affinity:
|
||||
# nodeAffinity:
|
||||
# requiredDuringSchedulingIgnoredDuringExecution:
|
||||
# nodeSelectorTerms:
|
||||
# - matchExpressions:
|
||||
# - key: kubernetes.io/arch
|
||||
# operator: In
|
||||
# values:
|
||||
# - amd64
|
||||
# - arm64
|
||||
# - ppc64le
|
||||
# - s390x
|
||||
# - key: kubernetes.io/os
|
||||
# operator: In
|
||||
# values:
|
||||
# - linux
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
# TODO(user): For common cases that do not require escalating privileges
|
||||
# it is recommended to ensure that all your Pods/Containers are restrictive.
|
||||
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
|
||||
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
|
||||
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
|
||||
# seccompProfile:
|
||||
# type: RuntimeDefault
|
||||
containers:
|
||||
- command:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
image: controller:latest
|
||||
image: 1password/onepassword-operator:latest
|
||||
name: manager
|
||||
env:
|
||||
- name: OPERATOR_NAME
|
||||
value: "onepassword-connect-operator"
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: WATCH_NAMESPACE
|
||||
value: "default"
|
||||
- name: POLLING_INTERVAL
|
||||
value: "10"
|
||||
- name: AUTO_RESTART
|
||||
value: "false"
|
||||
- name: OP_CONNECT_HOST
|
||||
value: "http://onepassword-connect:8080"
|
||||
- name: OP_CONNECT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: onepassword-token
|
||||
key: token
|
||||
- name: MANAGE_CONNECT
|
||||
value: "false"
|
||||
# Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN, OP_CONNECT_HOST and MANAGE_CONNECT env vars.
|
||||
# - name: OP_SERVICE_ACCOUNT_TOKEN
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: onepassword-service-account-token
|
||||
# key: token
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
@@ -52,9 +124,9 @@ spec:
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
serviceAccountName: controller-manager
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
serviceAccountName: onepassword-connect-operator
|
||||
terminationGracePeriodSeconds: 10
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# These resources constitute the fully configured set of manifests
|
||||
# used to generate the 'manifests/' directory in a bundle.
|
||||
resources:
|
||||
- bases/onepassword-operator-new.clusterserviceversion.yaml
|
||||
- bases/onepassword-operator.clusterserviceversion.yaml
|
||||
- ../default
|
||||
- ../samples
|
||||
- ../scorecard
|
||||
@@ -20,7 +20,8 @@ resources:
|
||||
# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs.
|
||||
# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment.
|
||||
# - op: remove
|
||||
# path: /spec/template/spec/containers/1/volumeMounts/0
|
||||
|
||||
# path: /spec/template/spec/containers/0/volumeMounts/0
|
||||
# # Remove the "cert" volume, since OLM will create and mount a set of certs.
|
||||
# # Update the indices in this path if adding or removing volumes in the manager's Deployment.
|
||||
# - op: remove
|
||||
|
@@ -1,11 +1,17 @@
|
||||
|
||||
# Prometheus Monitor Service (Metrics)
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
name: controller-manager-metrics-monitor
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
app.kubernetes.io/name: servicemonitor
|
||||
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:
|
||||
@@ -17,4 +23,5 @@ spec:
|
||||
insecureSkipVerify: true
|
||||
selector:
|
||||
matchLabels:
|
||||
control-plane: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
|
@@ -1,6 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: metrics-reader
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: metrics-reader
|
||||
rules:
|
||||
- nonResourceURLs:
|
||||
|
@@ -1,6 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: proxy-role
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: proxy-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
|
@@ -1,6 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrolebinding
|
||||
app.kubernetes.io/instance: proxy-rolebinding
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: proxy-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
@@ -8,5 +15,5 @@ roleRef:
|
||||
name: proxy-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
|
@@ -2,8 +2,15 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
name: controller-manager-metrics-service
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
app.kubernetes.io/name: service
|
||||
app.kubernetes.io/instance: controller-manager-metrics-service
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: onepassword-connect-operator-metrics-service
|
||||
namespace: system
|
||||
spec:
|
||||
ports:
|
||||
@@ -12,4 +19,5 @@ spec:
|
||||
protocol: TCP
|
||||
targetPort: https
|
||||
selector:
|
||||
control-plane: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
control-plane: onepassword-connect-operator
|
||||
|
@@ -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
|
||||
@@ -8,5 +15,5 @@ roleRef:
|
||||
name: leader-election-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
|
@@ -2,10 +2,17 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: onepassworditem-editor-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: onepassworditem-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- onepassword.onepassword.com
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems
|
||||
verbs:
|
||||
@@ -17,7 +24,7 @@ rules:
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- onepassword.onepassword.com
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems/status
|
||||
verbs:
|
||||
|
@@ -2,10 +2,17 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clusterrole
|
||||
app.kubernetes.io/instance: onepassworditem-viewer-role
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: onepassword-connect-operator
|
||||
app.kubernetes.io/part-of: onepassword-connect-operator
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: onepassworditem-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- onepassword.onepassword.com
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems
|
||||
verbs:
|
||||
@@ -13,7 +20,7 @@ rules:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- onepassword.onepassword.com
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems/status
|
||||
verbs:
|
||||
|
@@ -1,40 +1,21 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: onepassword-connect-operator-default
|
||||
namespace: default
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: onepassword-connect-operator
|
||||
namespace: default
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: onepassword-connect-operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: onepassword-connect-operator
|
||||
name: manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
- endpoints
|
||||
- events
|
||||
- namespaces
|
||||
- persistentvolumeclaims
|
||||
- pods
|
||||
- secrets
|
||||
- services
|
||||
- services/finalizers
|
||||
- endpoints
|
||||
- persistentvolumeclaims
|
||||
- events
|
||||
- configmaps
|
||||
- secrets
|
||||
- namespaces
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
@@ -43,11 +24,17 @@ rules:
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
- daemonsets
|
||||
- deployments
|
||||
- replicasets
|
||||
- statefulsets
|
||||
verbs:
|
||||
@@ -59,12 +46,30 @@ rules:
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- monitoring.coreos.com
|
||||
- apps
|
||||
resources:
|
||||
- servicemonitors
|
||||
- deployments
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
- replicasets
|
||||
verbs:
|
||||
- get
|
||||
- create
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- apps
|
||||
resourceNames:
|
||||
@@ -73,19 +78,30 @@ rules:
|
||||
- deployments/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- replicasets
|
||||
- deployments
|
||||
- deployments/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- update
|
||||
- apiGroups:
|
||||
- monitoring.coreos.com
|
||||
resources:
|
||||
- servicemonitors
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
@@ -97,4 +113,30 @@ rules:
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- watch
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
@@ -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
|
||||
@@ -8,5 +15,5 @@ roleRef:
|
||||
name: manager-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: controller-manager
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
|
@@ -1,5 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: controller-manager
|
||||
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.onepassword.com/v1
|
||||
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:
|
||||
# TODO(user): Add fields here
|
||||
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.19.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.19.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.19.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.19.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.19.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.19.0
|
||||
image: quay.io/operator-framework/scorecard-test:v1.33.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-status-descriptors-test
|
||||
|
@@ -1,235 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
"regexp"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
)
|
||||
|
||||
var deploymentLog = logf.Log.WithName("controller_deployment")
|
||||
var finalizer = "onepassword.com/finalizer.secret"
|
||||
|
||||
const annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
|
||||
|
||||
func Add(mgr manager.Manager, opConnectClient connect.Client) error {
|
||||
return add(mgr, newReconciler(mgr, opConnectClient))
|
||||
}
|
||||
|
||||
func newReconciler(mgr manager.Manager, opConnectClient connect.Client) *ReconcileDeployment {
|
||||
r, _ := regexp.Compile(annotationRegExpString)
|
||||
return &ReconcileDeployment{
|
||||
opAnnotationRegExp: r,
|
||||
kubeClient: mgr.GetClient(),
|
||||
scheme: mgr.GetScheme(),
|
||||
opConnectClient: opConnectClient,
|
||||
}
|
||||
}
|
||||
|
||||
func add(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
c, err := controller.New("deployment-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to primary resource Deployment
|
||||
err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ reconcile.Reconciler = &ReconcileDeployment{}
|
||||
|
||||
type ReconcileDeployment struct {
|
||||
opAnnotationRegExp *regexp.Regexp
|
||||
kubeClient client.Client
|
||||
scheme *runtime.Scheme
|
||||
opConnectClient connect.Client
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
||||
c, err := controller.New("deployment-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to primary resource Deployment
|
||||
err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// TODO figure out what to do with this code.
|
||||
// return ctrl.NewControllerManagedBy(mgr).
|
||||
// For(&appsv1.Deployment{}).
|
||||
// Complete(r)
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) test() {
|
||||
return
|
||||
}
|
||||
|
||||
// Reconcile reads that state of the cluster for a Deployment object and makes changes based on the state read
|
||||
// and what is in the Deployment.Spec
|
||||
// Note:
|
||||
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
|
||||
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
|
||||
func (r *ReconcileDeployment) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
|
||||
reqLogger := deploymentLog.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
reqLogger.Info("Reconciling Deployment")
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
err := r.kubeClient.Get(ctx, request.NamespacedName, deployment)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.opAnnotationRegExp)
|
||||
if !annotationsFound {
|
||||
reqLogger.Info("No 1Password Annotations found")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
//If the deployment is not being deleted
|
||||
if deployment.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// Adds a finalizer to the deployment if one does not exist.
|
||||
// This is so we can handle cleanup of associated secrets properly
|
||||
if !utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
|
||||
deployment.ObjectMeta.Finalizers = append(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
if err := r.kubeClient.Update(context.Background(), deployment); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
if err := r.HandleApplyingDeployment(deployment, deployment.Namespace, annotations, request); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
// The deployment has been marked for deletion. If the one password
|
||||
// finalizer is found there are cleanup tasks to perform
|
||||
if utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
|
||||
|
||||
secretName := annotations[op.NameAnnotation]
|
||||
r.cleanupKubernetesSecretForDeployment(secretName, deployment)
|
||||
|
||||
// Remove the finalizer from the deployment so deletion of deployment can be completed
|
||||
if err := r.removeOnePasswordFinalizerFromDeployment(deployment); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) cleanupKubernetesSecretForDeployment(secretName string, deletedDeployment *appsv1.Deployment) error {
|
||||
kubernetesSecret := &corev1.Secret{}
|
||||
kubernetesSecret.ObjectMeta.Name = secretName
|
||||
kubernetesSecret.ObjectMeta.Namespace = deletedDeployment.Namespace
|
||||
|
||||
if len(secretName) == 0 {
|
||||
return nil
|
||||
}
|
||||
updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret}
|
||||
|
||||
multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only delete the associated kubernetes secret if it is not being used by other deployments
|
||||
if !multipleDeploymentsUsingSecret {
|
||||
if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) {
|
||||
deployments := &appsv1.DeploymentList{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(deletedDeployment.Namespace),
|
||||
}
|
||||
|
||||
err := r.kubeClient.List(context.Background(), deployments, opts...)
|
||||
if err != nil {
|
||||
deploymentLog.Error(err, "Failed to list kubernetes deployments")
|
||||
return false, err
|
||||
}
|
||||
|
||||
for i := 0; i < len(deployments.Items); i++ {
|
||||
if deployments.Items[i].Name != deletedDeployment.Name {
|
||||
if op.IsDeploymentUsingSecrets(&deployments.Items[i], updatedSecrets) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deployment *appsv1.Deployment) error {
|
||||
deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
return r.kubeClient.Update(context.Background(), deployment)
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error {
|
||||
reqLog := deploymentLog.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
|
||||
secretName := annotations[op.NameAnnotation]
|
||||
secretLabels := map[string]string(nil)
|
||||
secretType := ""
|
||||
|
||||
if len(secretName) == 0 {
|
||||
reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.")
|
||||
return nil
|
||||
}
|
||||
|
||||
item, err := op.GetOnePasswordItemByPath(r.opConnectClient, annotations[op.ItemPathAnnotation])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve item: %v", err)
|
||||
}
|
||||
|
||||
// Create owner reference.
|
||||
gvk, err := apiutil.GVKForObject(deployment, r.scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not to retrieve group version kind: %v", err)
|
||||
}
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
Name: deployment.GetName(),
|
||||
UID: deployment.GetUID(),
|
||||
}
|
||||
|
||||
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, annotations, ownerRef)
|
||||
}
|
@@ -1,183 +0,0 @@
|
||||
/*
|
||||
Copyright 2022.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var log = logf.Log.WithName("controller_onepassworditem")
|
||||
|
||||
// OnePasswordItemReconciler reconciles a OnePasswordItem object
|
||||
type OnePasswordItemReconciler struct {
|
||||
Client kubeClient.Client
|
||||
Scheme *runtime.Scheme
|
||||
OpConnectClient connect.Client
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems/finalizers,verbs=update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the OnePasswordItem object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile
|
||||
func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
|
||||
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
reqLogger.Info("Reconciling OnePasswordItem")
|
||||
|
||||
onepassworditem := &onepasswordv1.OnePasswordItem{}
|
||||
err := r.Client.Get(context.Background(), request.NamespacedName, onepassworditem)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
// If the deployment is not being deleted
|
||||
if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// Adds a finalizer to the deployment if one does not exist.
|
||||
// This is so we can handle cleanup of associated secrets properly
|
||||
if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||
onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer)
|
||||
if err := r.Client.Update(context.Background(), onepassworditem); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
if err := r.HandleOnePasswordItem(onepassworditem, request); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
// If one password finalizer exists then we must cleanup associated secrets
|
||||
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||
|
||||
// Delete associated kubernetes secret
|
||||
if err = r.cleanupKubernetesSecret(onepassworditem); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
// Remove finalizer now that cleanup is complete
|
||||
if err := r.removeFinalizer(onepassworditem); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
c, err := controller.New("onepassworditem-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to primary resource OnePasswordItem
|
||||
err = c.Watch(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// TODO Consider the simplified code below. Based on the migration guide: https://sdk.operatorframework.io/docs/building-operators/golang/migration/#create-a-new-project
|
||||
// return ctrl.NewControllerManagedBy(mgr).Named("onepassworditem-controller").WithOptions(controller.Options{Reconciler: r}).
|
||||
// For(&onepasswordv1.OnePasswordItem{}).Watches(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{}).
|
||||
// Complete(r)
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer)
|
||||
if err := r.Client.Update(context.Background(), onePasswordItem); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
kubernetesSecret := &corev1.Secret{}
|
||||
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name
|
||||
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace
|
||||
|
||||
r.Client.Delete(context.Background(), kubernetesSecret)
|
||||
if err := r.Client.Delete(context.Background(), kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
|
||||
secretName := resource.GetName()
|
||||
labels := resource.Labels
|
||||
annotations := resource.Annotations
|
||||
secretType := resource.Type
|
||||
autoRestart := annotations[op.RestartDeploymentsAnnotation]
|
||||
|
||||
item, err := onepassword.GetOnePasswordItemByPath(r.OpConnectClient, resource.Spec.ItemPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve item: %v", err)
|
||||
}
|
||||
|
||||
// Create owner reference.
|
||||
gvk, err := apiutil.GVKForObject(resource, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not to retrieve group version kind: %v", err)
|
||||
}
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
Name: resource.GetName(),
|
||||
UID: resource.GetUID(),
|
||||
}
|
||||
|
||||
return kubeSecrets.CreateKubernetesSecretFromItem(r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, annotations, ownerRef)
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
Copyright 2022.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecsWithDefaultAndCustomReporters(t,
|
||||
"Controller Suite",
|
||||
[]Reporter{printer.NewlineReporter{}})
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
}
|
||||
|
||||
cfg, err := testEnv.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cfg).NotTo(BeNil())
|
||||
|
||||
err = onepasswordv1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
//+kubebuilder:scaffold:scheme
|
||||
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient).NotTo(BeNil())
|
||||
|
||||
}, 60)
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
131
go.mod
131
go.mod
@@ -1,81 +1,92 @@
|
||||
module github.com/1Password/onepassword-operator
|
||||
|
||||
go 1.17
|
||||
go 1.24
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/1Password/connect-sdk-go v1.2.0
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/onsi/gomega v1.17.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
k8s.io/api v0.23.5
|
||||
k8s.io/apimachinery v0.23.5
|
||||
k8s.io/client-go v0.23.5
|
||||
k8s.io/kubectl v0.23.5
|
||||
sigs.k8s.io/controller-runtime v0.11.0
|
||||
github.com/1Password/connect-sdk-go v1.5.3
|
||||
github.com/1password/onepassword-sdk-go v0.3.1
|
||||
github.com/onsi/ginkgo/v2 v2.14.0
|
||||
github.com/onsi/gomega v1.30.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
k8s.io/api v0.29.3
|
||||
k8s.io/apimachinery v0.29.3
|
||||
k8s.io/client-go v0.29.3
|
||||
k8s.io/kubectl v0.29.0
|
||||
sigs.k8s.io/controller-runtime v0.17.2
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.81.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.18 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/go-logr/logr v1.2.0 // indirect
|
||||
github.com/go-logr/zapr v1.2.0 // indirect
|
||||
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/extism/go-sdk v1.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.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 v0.0.0-20230315185526-52ccab3ef572 // 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.2 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // 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.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.28.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/prometheus/client_golang v1.19.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.0 // indirect
|
||||
github.com/prometheus/common v0.51.1 // indirect
|
||||
github.com/prometheus/procfs v0.13.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect
|
||||
github.com/uber/jaeger-lib v2.4.0+incompatible // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.19.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
|
||||
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
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.opentelemetry.io/proto/otlp v1.3.1 // 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-20240318143956-a85f2c67cd81 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/term v0.23.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
k8s.io/apiextensions-apiserver v0.23.0 // indirect
|
||||
k8s.io/component-base v0.23.5 // indirect
|
||||
k8s.io/klog/v2 v2.30.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
|
||||
k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.29.3 // indirect
|
||||
k8s.io/component-base v0.29.3 // indirect
|
||||
k8s.io/klog/v2 v2.120.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 // indirect
|
||||
k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
@@ -1,15 +1,23 @@
|
||||
/*
|
||||
Copyright 2022.
|
||||
MIT License
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
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:
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
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.
|
||||
*/
|
222
internal/controller/deployment_controller.go
Normal file
222
internal/controller/deployment_controller.go
Normal file
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"github.com/1Password/onepassword-operator/pkg/logs"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
var logDeployment = logf.Log.WithName("controller_deployment")
|
||||
|
||||
// DeploymentReconciler reconciles a Deployment object
|
||||
type DeploymentReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
OpClient opclient.Client
|
||||
OpAnnotationRegExp *regexp.Regexp
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the OnePasswordItem object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
|
||||
func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
reqLogger := logDeployment.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name)
|
||||
reqLogger.V(logs.DebugLevel).Info("Reconciling Deployment")
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
err := r.Get(ctx, req.NamespacedName, deployment)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.OpAnnotationRegExp)
|
||||
if !annotationsFound {
|
||||
reqLogger.V(logs.DebugLevel).Info("No 1Password Annotations found")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
//If the deployment is not being deleted
|
||||
if deployment.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// Adds a finalizer to the deployment if one does not exist.
|
||||
// This is so we can handle cleanup of associated secrets properly
|
||||
if !utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
|
||||
deployment.ObjectMeta.Finalizers = append(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
if err = r.Update(ctx, deployment); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
if err = r.handleApplyingDeployment(ctx, deployment, deployment.Namespace, annotations, req); err != nil {
|
||||
if strings.Contains(err.Error(), "rate limit") {
|
||||
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
|
||||
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
|
||||
} else {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
// The deployment has been marked for deletion. If the one password
|
||||
// finalizer is found there are cleanup tasks to perform
|
||||
if utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
|
||||
|
||||
secretName := annotations[op.NameAnnotation]
|
||||
if err = r.cleanupKubernetesSecretForDeployment(ctx, secretName, deployment); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Remove the finalizer from the deployment so deletion of deployment can be completed
|
||||
if err = r.removeOnePasswordFinalizerFromDeployment(ctx, deployment); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&appsv1.Deployment{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if len(secretName) == 0 {
|
||||
return nil
|
||||
}
|
||||
updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret}
|
||||
|
||||
multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(ctx, updatedSecrets, *deletedDeployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only delete the associated kubernetes secret if it is not being used by other deployments
|
||||
if !multipleDeploymentsUsingSecret {
|
||||
if err = r.Delete(ctx, kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DeploymentReconciler) areMultipleDeploymentsUsingSecret(ctx context.Context, updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) {
|
||||
deployments := &appsv1.DeploymentList{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(deletedDeployment.Namespace),
|
||||
}
|
||||
|
||||
err := r.List(ctx, deployments, opts...)
|
||||
if err != nil {
|
||||
logDeployment.Error(err, "Failed to list kubernetes deployments")
|
||||
return false, err
|
||||
}
|
||||
|
||||
for i := 0; i < len(deployments.Items); i++ {
|
||||
if deployments.Items[i].Name != deletedDeployment.Name {
|
||||
if op.IsDeploymentUsingSecrets(&deployments.Items[i], updatedSecrets) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *DeploymentReconciler) removeOnePasswordFinalizerFromDeployment(ctx context.Context, deployment *appsv1.Deployment) error {
|
||||
deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
return r.Update(ctx, deployment)
|
||||
}
|
||||
|
||||
func (r *DeploymentReconciler) handleApplyingDeployment(ctx context.Context, deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error {
|
||||
reqLog := logDeployment.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
|
||||
secretName := annotations[op.NameAnnotation]
|
||||
secretLabels := map[string]string(nil)
|
||||
secretType := string(corev1.SecretTypeOpaque)
|
||||
|
||||
if len(secretName) == 0 {
|
||||
reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.")
|
||||
return nil
|
||||
}
|
||||
|
||||
item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, annotations[op.ItemPathAnnotation])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve item: %v", 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)
|
||||
}
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
Name: deployment.GetName(),
|
||||
UID: deployment.GetUID(),
|
||||
}
|
||||
|
||||
return kubeSecrets.CreateKubernetesSecretFromItem(ctx, r.Client, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef)
|
||||
}
|
399
internal/controller/deployment_controller_test.go
Normal file
399
internal/controller/deployment_controller_test.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentKind = "Deployment"
|
||||
deploymentAPIVersion = "v1"
|
||||
deploymentName = "test-deployment"
|
||||
)
|
||||
|
||||
var _ = Describe("Deployment controller", func() {
|
||||
ctx := context.Background()
|
||||
var deploymentKey types.NamespacedName
|
||||
var secretKey types.NamespacedName
|
||||
var deploymentResource *appsv1.Deployment
|
||||
createdSecret := &v1.Secret{}
|
||||
|
||||
makeDeployment := func() {
|
||||
|
||||
deploymentKey = types.NamespacedName{
|
||||
Name: deploymentName,
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
secretKey = types.NamespacedName{
|
||||
Name: item1.Name,
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
By("Deploying a pod with proper annotations successfully")
|
||||
deploymentResource = &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: deploymentKey.Name,
|
||||
Namespace: deploymentKey.Namespace,
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: item1.Path,
|
||||
op.NameAnnotation: item1.Name,
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": deploymentName},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: deploymentName,
|
||||
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": deploymentName},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, deploymentResource)).Should(Succeed())
|
||||
|
||||
By("Creating the K8s secret successfully")
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, secretKey, createdSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(createdSecret.Data).Should(Equal(item1.SecretData))
|
||||
}
|
||||
|
||||
cleanK8sResources := func() {
|
||||
// failed test runs that don't clean up leave resources behind.
|
||||
err := k8sClient.DeleteAllOf(ctx, &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = k8sClient.DeleteAllOf(ctx, &v1.Secret{}, client.InNamespace(namespace))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = k8sClient.DeleteAllOf(ctx, &appsv1.Deployment{}, client.InNamespace(namespace))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
mockGetItemFunc := func() {
|
||||
// mock GetItemByID to return test item 'item1'
|
||||
mockGetItemByIDFunc.Return(item1.ToModel(), nil)
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
cleanK8sResources()
|
||||
mockGetItemFunc()
|
||||
time.Sleep(time.Second) // TODO: can we achieve that with ginkgo?
|
||||
makeDeployment()
|
||||
})
|
||||
|
||||
Context("Deployment with secrets from 1Password", func() {
|
||||
It("Should delete secret if deployment is deleted", func() {
|
||||
By("Deleting the pod")
|
||||
Eventually(func() error {
|
||||
f := &appsv1.Deployment{}
|
||||
err := k8sClient.Get(ctx, deploymentKey, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return k8sClient.Delete(ctx, f)
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
f := &appsv1.Deployment{}
|
||||
return k8sClient.Get(ctx, deploymentKey, f)
|
||||
}, timeout, interval).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
f := &v1.Secret{}
|
||||
return k8sClient.Get(ctx, secretKey, f)
|
||||
}, timeout, interval).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("Should update existing K8s Secret using deployment", func() {
|
||||
By("Updating secret")
|
||||
|
||||
// mock GetItemByID to return test item 'item2'
|
||||
mockGetItemByIDFunc.Return(item2.ToModel(), nil)
|
||||
|
||||
Eventually(func() error {
|
||||
updatedDeployment := &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: deploymentKey.Name,
|
||||
Namespace: deploymentKey.Namespace,
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: item2.Path,
|
||||
op.NameAnnotation: item1.Name,
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": deploymentName},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: deploymentName,
|
||||
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": deploymentName},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := k8sClient.Update(ctx, updatedDeployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
// TODO: can we achieve the same without sleep?
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
By("Reading updated K8s secret")
|
||||
updatedSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, secretKey, updatedSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(updatedSecret.Data).Should(Equal(item2.SecretData))
|
||||
})
|
||||
|
||||
It("Should not update secret if Annotations have not changed", func() {
|
||||
By("Updating secret without changing annotations")
|
||||
Eventually(func() error {
|
||||
updatedDeployment := &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: deploymentKey.Name,
|
||||
Namespace: deploymentKey.Namespace,
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: item1.Path,
|
||||
op.NameAnnotation: item1.Name,
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": deploymentName},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: deploymentName,
|
||||
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": deploymentName},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := k8sClient.Update(ctx, updatedDeployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
// TODO: can we achieve the same without sleep?
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
By("Reading updated K8s secret")
|
||||
updatedSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, secretKey, updatedSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(updatedSecret.Data).Should(Equal(item1.SecretData))
|
||||
})
|
||||
|
||||
It("Should not delete secret created via deployment if it's used in another container", func() {
|
||||
By("Creating another POD with created secret")
|
||||
anotherDeploymentKey := types.NamespacedName{
|
||||
Name: "other-deployment",
|
||||
Namespace: namespace,
|
||||
}
|
||||
Eventually(func() error {
|
||||
anotherDeployment := &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: anotherDeploymentKey.Name,
|
||||
Namespace: anotherDeploymentKey.Namespace,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": anotherDeploymentKey.Name},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: anotherDeploymentKey.Name,
|
||||
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
Env: []v1.EnvVar{
|
||||
{
|
||||
Name: anotherDeploymentKey.Name,
|
||||
ValueFrom: &v1.EnvVarSource{
|
||||
SecretKeyRef: &v1.SecretKeySelector{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: secretKey.Name,
|
||||
},
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": anotherDeploymentKey.Name},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := k8sClient.Create(ctx, anotherDeployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
By("Deleting the pod")
|
||||
Eventually(func() error {
|
||||
f := &appsv1.Deployment{}
|
||||
err := k8sClient.Get(ctx, deploymentKey, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return k8sClient.Delete(ctx, f)
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
f := &v1.Secret{}
|
||||
return k8sClient.Get(ctx, secretKey, f)
|
||||
}, timeout, interval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Should not delete secret created via deployment if it's used in another volume", func() {
|
||||
By("Creating another POD with created secret")
|
||||
anotherDeploymentKey := types.NamespacedName{
|
||||
Name: "other-deployment",
|
||||
Namespace: namespace,
|
||||
}
|
||||
Eventually(func() error {
|
||||
anotherDeployment := &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: anotherDeploymentKey.Name,
|
||||
Namespace: anotherDeploymentKey.Namespace,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": anotherDeploymentKey.Name},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Volumes: []v1.Volume{
|
||||
{
|
||||
Name: anotherDeploymentKey.Name,
|
||||
VolumeSource: v1.VolumeSource{
|
||||
Secret: &v1.SecretVolumeSource{
|
||||
SecretName: secretKey.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: anotherDeploymentKey.Name,
|
||||
Image: "eu.gcr.io/kyma-project/example/http-db-service:0.0.6",
|
||||
ImagePullPolicy: "IfNotPresent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": anotherDeploymentKey.Name},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := k8sClient.Create(ctx, anotherDeployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
By("Deleting the pod")
|
||||
Eventually(func() error {
|
||||
f := &appsv1.Deployment{}
|
||||
err := k8sClient.Get(ctx, deploymentKey, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return k8sClient.Delete(ctx, f)
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
f := &v1.Secret{}
|
||||
return k8sClient.Get(ctx, secretKey, f)
|
||||
}, timeout, interval).Should(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
223
internal/controller/onepassworditem_controller.go
Normal file
223
internal/controller/onepassworditem_controller.go
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"github.com/1Password/onepassword-operator/pkg/logs"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
var logOnePasswordItem = logf.Log.WithName("controller_onepassworditem")
|
||||
var finalizer = "onepassword.com/finalizer.secret"
|
||||
|
||||
// OnePasswordItemReconciler reconciles a OnePasswordItem object
|
||||
type OnePasswordItemReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
OpClient opclient.Client
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/finalizers,verbs=update
|
||||
|
||||
//+kubebuilder:rbac:groups="",resources=pods,verbs=get
|
||||
//+kubebuilder:rbac:groups="",resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets;namespaces,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=daemonsets;deployments;replicasets;statefulsets,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=replicasets;deployments,verbs=get
|
||||
//+kubebuilder:rbac:groups=apps,resourceNames=onepassword-connect-operator,resources=deployments/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups=onepassword.com,resources=*,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create
|
||||
//+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the OnePasswordItem object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
|
||||
func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
reqLogger := logOnePasswordItem.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name)
|
||||
reqLogger.V(logs.DebugLevel).Info("Reconciling OnePasswordItem")
|
||||
|
||||
onepassworditem := &onepasswordv1.OnePasswordItem{}
|
||||
err := r.Get(ctx, req.NamespacedName, onepassworditem)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// If the deployment is not being deleted
|
||||
if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// Adds a finalizer to the deployment if one does not exist.
|
||||
// This is so we can handle cleanup of associated secrets properly
|
||||
if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||
onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer)
|
||||
if err = r.Update(ctx, onepassworditem); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
err = r.handleOnePasswordItem(ctx, onepassworditem, req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "rate limit") {
|
||||
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
|
||||
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
|
||||
}
|
||||
}
|
||||
if updateStatusErr := r.updateStatus(ctx, onepassworditem, err); updateStatusErr != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr)
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// If one password finalizer exists then we must cleanup associated secrets
|
||||
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||
|
||||
// Delete associated kubernetes secret
|
||||
if err = r.cleanupKubernetesSecret(ctx, onepassworditem); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Remove finalizer now that cleanup is complete
|
||||
if err = r.removeFinalizer(ctx, onepassworditem); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&onepasswordv1.OnePasswordItem{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) removeFinalizer(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer)
|
||||
if err := r.Update(ctx, onePasswordItem); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
kubernetesSecret := &corev1.Secret{}
|
||||
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name
|
||||
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace
|
||||
|
||||
if err := r.Delete(ctx, kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(ctx context.Context, opSecret *onepasswordv1.OnePasswordItem) error {
|
||||
opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer)
|
||||
return r.Update(ctx, opSecret)
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) handleOnePasswordItem(ctx context.Context, resource *onepasswordv1.OnePasswordItem, req ctrl.Request) error {
|
||||
secretName := resource.GetName()
|
||||
labels := resource.Labels
|
||||
secretType := resource.Type
|
||||
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
|
||||
|
||||
item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, resource.Spec.ItemPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve item: %v", err)
|
||||
}
|
||||
|
||||
// Create owner reference.
|
||||
gvk, err := apiutil.GVKForObject(resource, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not to retrieve group version kind: %v", err)
|
||||
}
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
Name: resource.GetName(),
|
||||
UID: resource.GetUID(),
|
||||
}
|
||||
|
||||
return kubeSecrets.CreateKubernetesSecretFromItem(ctx, r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef)
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) updateStatus(ctx context.Context, resource *onepasswordv1.OnePasswordItem, err error) error {
|
||||
existingCondition := findCondition(resource.Status.Conditions, onepasswordv1.OnePasswordItemReady)
|
||||
updatedCondition := existingCondition
|
||||
if err != nil {
|
||||
updatedCondition.Message = err.Error()
|
||||
updatedCondition.Status = metav1.ConditionFalse
|
||||
} else {
|
||||
updatedCondition.Message = ""
|
||||
updatedCondition.Status = metav1.ConditionTrue
|
||||
}
|
||||
|
||||
if existingCondition.Status != updatedCondition.Status {
|
||||
updatedCondition.LastTransitionTime = metav1.Now()
|
||||
}
|
||||
|
||||
resource.Status.Conditions = []onepasswordv1.OnePasswordItemCondition{updatedCondition}
|
||||
return r.Status().Update(ctx, resource)
|
||||
}
|
||||
|
||||
func findCondition(conditions []onepasswordv1.OnePasswordItemCondition, t onepasswordv1.OnePasswordItemConditionType) onepasswordv1.OnePasswordItemCondition {
|
||||
for _, c := range conditions {
|
||||
if c.Type == t {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return onepasswordv1.OnePasswordItemCondition{
|
||||
Type: t,
|
||||
Status: metav1.ConditionUnknown,
|
||||
}
|
||||
}
|
403
internal/controller/onepassworditem_controller_test.go
Normal file
403
internal/controller/onepassworditem_controller_test.go
Normal file
@@ -0,0 +1,403 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
|
||||
)
|
||||
|
||||
const (
|
||||
firstHost = "http://localhost:8080"
|
||||
awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
iceCream = "freezing blue 20%"
|
||||
)
|
||||
|
||||
var _ = Describe("OnePasswordItem controller", func() {
|
||||
BeforeEach(func() {
|
||||
// failed test runs that don't clean up leave resources behind.
|
||||
err := k8sClient.DeleteAllOf(context.Background(), &onepasswordv1.OnePasswordItem{}, client.InNamespace(namespace))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
item := item1.ToModel()
|
||||
mockGetItemByIDFunc.Return(item, nil)
|
||||
})
|
||||
|
||||
Context("Happy path", func() {
|
||||
It("Should handle 1Password Item and secret correctly", func() {
|
||||
ctx := context.Background()
|
||||
spec := onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: item1.Path,
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Name: "sample-item",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
toCreate := &onepasswordv1.OnePasswordItem{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
By("Creating a new OnePasswordItem successfully")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
|
||||
|
||||
created := &onepasswordv1.OnePasswordItem{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, created)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, 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
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(createdSecret.Data).Should(Equal(item1.SecretData))
|
||||
|
||||
By("Updating existing secret successfully")
|
||||
newData := map[string]string{
|
||||
"username": "newUser1234",
|
||||
"password": "##newPassword##",
|
||||
"extraField": "dev",
|
||||
}
|
||||
newDataByte := map[string][]byte{
|
||||
"username": []byte("newUser1234"),
|
||||
"password": []byte("##newPassword##"),
|
||||
"extraField": []byte("dev"),
|
||||
}
|
||||
|
||||
item := item2.ToModel()
|
||||
for k, v := range newData {
|
||||
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
|
||||
}
|
||||
mockGetItemByIDFunc.Return(item, nil)
|
||||
|
||||
_, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
updatedSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, updatedSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(updatedSecret.Data).Should(Equal(newDataByte))
|
||||
|
||||
By("Deleting the OnePasswordItem successfully")
|
||||
Eventually(func() error {
|
||||
f := &onepasswordv1.OnePasswordItem{}
|
||||
err := k8sClient.Get(ctx, key, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return k8sClient.Delete(ctx, f)
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
f := &onepasswordv1.OnePasswordItem{}
|
||||
return k8sClient.Get(ctx, key, f)
|
||||
}, timeout, interval).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
f := &v1.Secret{}
|
||||
return k8sClient.Get(ctx, key, f)
|
||||
}, timeout, interval).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("Should handle 1Password Item with fields and sections that have invalid K8s labels correctly", func() {
|
||||
ctx := context.Background()
|
||||
spec := onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: item1.Path,
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Name: "my-secret-it3m",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
toCreate := &onepasswordv1.OnePasswordItem{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
testData := map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"first host": firstHost,
|
||||
"AWS Access Key": awsKey,
|
||||
"😄 ice-cream type": iceCream,
|
||||
}
|
||||
expectedData := map[string][]byte{
|
||||
"username": []byte(username),
|
||||
"password": []byte(password),
|
||||
"first-host": []byte(firstHost),
|
||||
"AWS-Access-Key": []byte(awsKey),
|
||||
"ice-cream-type": []byte(iceCream),
|
||||
}
|
||||
|
||||
item := item2.ToModel()
|
||||
for k, v := range testData {
|
||||
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
|
||||
}
|
||||
mockGetItemByIDFunc.Return(item, nil)
|
||||
|
||||
By("Creating a new OnePasswordItem successfully")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
|
||||
|
||||
created := &onepasswordv1.OnePasswordItem{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, created)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, 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
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(createdSecret.Data).Should(Equal(expectedData))
|
||||
|
||||
By("Deleting the OnePasswordItem successfully")
|
||||
Eventually(func() error {
|
||||
f := &onepasswordv1.OnePasswordItem{}
|
||||
err := k8sClient.Get(ctx, key, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return k8sClient.Delete(ctx, f)
|
||||
}, timeout, interval).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
f := &onepasswordv1.OnePasswordItem{}
|
||||
return k8sClient.Get(ctx, key, f)
|
||||
}, timeout, interval).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
f := &v1.Secret{}
|
||||
return k8sClient.Get(ctx, key, f)
|
||||
}, timeout, interval).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("Should not update K8s secret if OnePasswordItem Version or VaultPath has not changed", func() {
|
||||
ctx := context.Background()
|
||||
spec := onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: item1.Path,
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Name: "item-not-updated",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
toCreate := &onepasswordv1.OnePasswordItem{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
By("Creating a new OnePasswordItem successfully")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
|
||||
|
||||
item := &onepasswordv1.OnePasswordItem{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, item)
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
By("Creating the K8s secret successfully")
|
||||
createdSecret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, createdSecret)
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(createdSecret.Data).Should(Equal(item1.SecretData))
|
||||
|
||||
By("Updating OnePasswordItem type")
|
||||
Eventually(func() bool {
|
||||
err1 := k8sClient.Get(ctx, key, item)
|
||||
if err1 != nil {
|
||||
return false
|
||||
}
|
||||
item.Type = string(v1.SecretTypeOpaque)
|
||||
err := k8sClient.Update(ctx, item)
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
|
||||
By("Reading K8s secret")
|
||||
secret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, secret)
|
||||
return err == nil
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(secret.Data).Should(Equal(item1.SecretData))
|
||||
})
|
||||
|
||||
It("Should create custom K8s Secret type using OnePasswordItem", func() {
|
||||
const customType = "CustomType"
|
||||
ctx := context.Background()
|
||||
spec := onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: item1.Path,
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Name: "item-custom-secret-type",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
toCreate := &onepasswordv1.OnePasswordItem{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
Type: customType,
|
||||
}
|
||||
|
||||
By("Creating a new OnePasswordItem successfully")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
|
||||
|
||||
By("Reading K8s secret")
|
||||
secret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, secret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, timeout, interval).Should(BeTrue())
|
||||
Expect(secret.Type).Should(Equal(v1.SecretType(customType)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Unhappy path", func() {
|
||||
It("Should throw an error if K8s Secret type is changed", func() {
|
||||
ctx := context.Background()
|
||||
spec := onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: item1.Path,
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Name: "item-changed-secret-type",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
toCreate := &onepasswordv1.OnePasswordItem{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
By("Creating a new OnePasswordItem successfully")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())
|
||||
|
||||
By("Reading K8s secret")
|
||||
secret := &v1.Secret{}
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(ctx, key, secret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, 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
|
||||
}, timeout, interval).Should(BeFalse())
|
||||
})
|
||||
|
||||
When("OnePasswordItem resource name contains `_`", func() {
|
||||
It("Should fail creating a OnePasswordItem resource", func() {
|
||||
ctx := context.Background()
|
||||
spec := onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: item1.Path,
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Name: "invalid_name",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
toCreate := &onepasswordv1.OnePasswordItem{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
By("Creating a new OnePasswordItem")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).To(HaveOccurred())
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
When("OnePasswordItem resource name contains capital letters", func() {
|
||||
It("Should fail creating a OnePasswordItem resource", func() {
|
||||
ctx := context.Background()
|
||||
spec := onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: item1.Path,
|
||||
}
|
||||
|
||||
key := types.NamespacedName{
|
||||
Name: "invalidName",
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
toCreate := &onepasswordv1.OnePasswordItem{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
By("Creating a new OnePasswordItem")
|
||||
Expect(k8sClient.Create(ctx, toCreate)).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
212
internal/controller/suite_test.go
Normal file
212
internal/controller/suite_test.go
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2024 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
const (
|
||||
username = "test-user"
|
||||
password = "QmHumKc$mUeEem7caHtbaBaJ"
|
||||
|
||||
username2 = "test-user2"
|
||||
password2 = "4zotzqDqXKasLFT2jzTs"
|
||||
|
||||
annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
|
||||
)
|
||||
|
||||
// Define utility constants for object names and testing timeouts/durations and intervals.
|
||||
const (
|
||||
namespace = "default"
|
||||
|
||||
timeout = time.Second * 10
|
||||
duration = time.Second * 10
|
||||
interval = time.Millisecond * 250
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *rest.Config
|
||||
k8sClient client.Client
|
||||
testEnv *envtest.Environment
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
onePasswordItemReconciler *OnePasswordItemReconciler
|
||||
deploymentReconciler *DeploymentReconciler
|
||||
mockGetItemByIDFunc *mock.Call
|
||||
|
||||
item1 = &TestItem{
|
||||
ItemID: "nwrhuano7bcwddcviubpp4mhfq",
|
||||
VaultID: "hfnjvi6aymbsnfc2xeeoheizda",
|
||||
Name: "test-item",
|
||||
Version: 123,
|
||||
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizda/items/nwrhuano7bcwddcviubpp4mhfq",
|
||||
Data: map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
SecretData: map[string][]byte{
|
||||
"password": []byte(password),
|
||||
"username": []byte(username),
|
||||
},
|
||||
}
|
||||
|
||||
item2 = &TestItem{
|
||||
ItemID: "nwrhuano7bcwddcviubpp4mhf2",
|
||||
VaultID: "hfnjvi6aymbsnfc2xeeoheizd2",
|
||||
Name: "test-item2",
|
||||
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizd2/items/nwrhuano7bcwddcviubpp4mhf2",
|
||||
Version: 456,
|
||||
Data: map[string]string{
|
||||
"username": username2,
|
||||
"password": password2,
|
||||
},
|
||||
SecretData: map[string][]byte{
|
||||
"password": []byte(password2),
|
||||
"username": []byte(username2),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type TestItem struct {
|
||||
ItemID string
|
||||
VaultID string
|
||||
Name string
|
||||
Version int
|
||||
Path string
|
||||
Data map[string]string
|
||||
SecretData map[string][]byte
|
||||
}
|
||||
|
||||
func (ti *TestItem) ToModel() *model.Item {
|
||||
item := &model.Item{}
|
||||
item.Version = ti.Version
|
||||
item.VaultID = ti.VaultID
|
||||
item.ID = ti.ItemID
|
||||
|
||||
item.Fields = []model.ItemField{}
|
||||
for k, v := range ti.Data {
|
||||
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||
|
||||
ctx, cancel = context.WithCancel(context.TODO())
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
}
|
||||
|
||||
var err error
|
||||
// cfg is defined in this file globally.
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cfg).NotTo(BeNil())
|
||||
|
||||
err = onepasswordcomv1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
//+kubebuilder:scaffold:scheme
|
||||
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient).NotTo(BeNil())
|
||||
|
||||
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
|
||||
Scheme: scheme.Scheme,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
mockOpClient := &mocks.TestClient{}
|
||||
mockGetItemByIDFunc = mockOpClient.On("GetItemByID", mock.Anything, mock.Anything)
|
||||
|
||||
onePasswordItemReconciler = &OnePasswordItemReconciler{
|
||||
Client: k8sManager.GetClient(),
|
||||
Scheme: k8sManager.GetScheme(),
|
||||
OpClient: mockOpClient,
|
||||
}
|
||||
err = (onePasswordItemReconciler).SetupWithManager(k8sManager)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
r, _ := regexp.Compile(annotationRegExpString)
|
||||
deploymentReconciler = &DeploymentReconciler{
|
||||
Client: k8sManager.GetClient(),
|
||||
Scheme: k8sManager.GetScheme(),
|
||||
OpClient: mockOpClient,
|
||||
OpAnnotationRegExp: r,
|
||||
}
|
||||
err = (deploymentReconciler).SetupWithManager(k8sManager)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
err = k8sManager.Start(ctx)
|
||||
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
|
||||
}()
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
cancel()
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
@@ -2,17 +2,13 @@ package kubernetessecrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
errs "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"
|
||||
@@ -27,7 +23,6 @@ import (
|
||||
const OnepasswordPrefix = "operator.1password.io"
|
||||
const NameAnnotation = OnepasswordPrefix + "/item-name"
|
||||
const VersionAnnotation = OnepasswordPrefix + "/item-version"
|
||||
const restartAnnotation = OnepasswordPrefix + "/last-restarted"
|
||||
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
|
||||
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
|
||||
|
||||
@@ -35,23 +30,17 @@ var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type
|
||||
|
||||
var log = logf.Log
|
||||
|
||||
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string, ownerRef *metav1.OwnerReference) error {
|
||||
|
||||
func CreateKubernetesSecretFromItem(ctx context.Context, kubeClient kubernetesClient.Client, secretName, namespace string, item *model.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error {
|
||||
itemVersion := fmt.Sprint(item.Version)
|
||||
|
||||
// If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map
|
||||
if secretAnnotations == nil {
|
||||
secretAnnotations = map[string]string{}
|
||||
secretAnnotations := map[string]string{
|
||||
VersionAnnotation: itemVersion,
|
||||
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID),
|
||||
}
|
||||
|
||||
secretAnnotations[VersionAnnotation] = itemVersion
|
||||
secretAnnotations[ItemPathAnnotation] = fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
|
||||
|
||||
if autoRestart != "" {
|
||||
_, err := utils.StringToBool(autoRestart)
|
||||
if err != nil {
|
||||
log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName)
|
||||
return err
|
||||
return fmt.Errorf("Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName)
|
||||
}
|
||||
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
|
||||
}
|
||||
@@ -60,34 +49,46 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
|
||||
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)
|
||||
err := kubeClient.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
|
||||
if err != nil && errors.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
|
||||
}
|
||||
|
||||
currentAnnotations := currentSecret.Annotations
|
||||
currentLabels := currentSecret.Labels
|
||||
// Check if the secret types are being changed on the update.
|
||||
// Avoid Opaque and "" are treated as different on check.
|
||||
wantSecretType := secretType
|
||||
if wantSecretType == "" {
|
||||
wantSecretType = string(corev1.SecretTypeOpaque)
|
||||
}
|
||||
currentSecretType := string(currentSecret.Type)
|
||||
if !reflect.DeepEqual(currentSecretType, secretType) {
|
||||
if currentSecretType == "" {
|
||||
currentSecretType = string(corev1.SecretTypeOpaque)
|
||||
}
|
||||
if currentSecretType != wantSecretType {
|
||||
return ErrCannotUpdateSecretType
|
||||
}
|
||||
|
||||
currentAnnotations := currentSecret.Annotations
|
||||
currentLabels := currentSecret.Labels
|
||||
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) {
|
||||
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
|
||||
currentSecret.ObjectMeta.Annotations = secretAnnotations
|
||||
currentSecret.ObjectMeta.Labels = labels
|
||||
currentSecret.Data = secret.Data
|
||||
return kubeClient.Update(context.Background(), currentSecret)
|
||||
if err := kubeClient.Update(ctx, currentSecret); err != nil {
|
||||
return fmt.Errorf("Kubernetes secret update failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation]))
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, 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}
|
||||
@@ -106,7 +107,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 != "" {
|
||||
|
@@ -3,74 +3,61 @@ package kubernetessecrets
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
const restartDeploymentAnnotation = "false"
|
||||
|
||||
type k8s struct {
|
||||
clientset kubernetes.Interface
|
||||
}
|
||||
|
||||
func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
secretName := "test-secret-name"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
secretAnnotations := map[string]string{
|
||||
"testAnnotation": "exists",
|
||||
}
|
||||
secretType := ""
|
||||
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, 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)
|
||||
}
|
||||
compareFields(item.Fields, createdSecret.Data, t)
|
||||
compareAnnotationsToItem(createdSecret.Annotations, item, t)
|
||||
|
||||
if createdSecret.Annotations["testAnnotation"] != "exists" {
|
||||
t.Errorf("Expected testAnnotation to be merged with existing annotations, but wasn't.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
secretName := "test-secret-name"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
secretAnnotations := map[string]string{
|
||||
"testAnnotation": "exists",
|
||||
}
|
||||
secretType := ""
|
||||
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
@@ -79,12 +66,12 @@ func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
|
||||
Name: "test-deployment",
|
||||
UID: types.UID("test-uid"),
|
||||
}
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, 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)
|
||||
|
||||
// Check owner references.
|
||||
gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences
|
||||
@@ -105,38 +92,38 @@ func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
secretName := "test-secret-update"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
secretAnnotations := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, 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.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil)
|
||||
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)
|
||||
@@ -162,7 +149,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 := ""
|
||||
@@ -188,10 +175,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",
|
||||
@@ -221,28 +208,26 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
secretName := "tls-test-secret-name"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item := model.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
secretAnnotations := map[string]string{
|
||||
"testAnnotation": "exists",
|
||||
}
|
||||
secretType := "kubernetes.io/tls"
|
||||
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil)
|
||||
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)
|
||||
@@ -253,13 +238,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)
|
||||
@@ -273,7 +258,7 @@ func compareAnnotationsToItem(annotations map[string]string, item onepassword.It
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -285,14 +270,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
|
||||
}
|
||||
|
11
pkg/logs/log_levels.go
Normal file
11
pkg/logs/log_levels.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package logs
|
||||
|
||||
// A Level is a logging priority. Lower levels are more important.
|
||||
// All levels have been multipled by -1 to ensure compatibilty
|
||||
// between zapcore and logr
|
||||
const (
|
||||
ErrorLevel = -2
|
||||
WarnLevel = -1
|
||||
InfoLevel = 0
|
||||
DebugLevel = 1
|
||||
)
|
@@ -1,84 +1,38 @@
|
||||
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)
|
||||
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
|
||||
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
|
||||
GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
|
||||
GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
|
||||
CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
|
||||
GetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error)
|
||||
GetFileContentFunc func(file *onepassword.File) ([]byte, error)
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
var (
|
||||
GetGetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
DoGetVaultFunc func(uuid string) (*onepassword.Vault, error)
|
||||
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
|
||||
DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
|
||||
DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
|
||||
DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
|
||||
DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
|
||||
DoUpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DoGetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error)
|
||||
DoGetFileContentFunc func(file *onepassword.File) ([]byte, error)
|
||||
)
|
||||
|
||||
// Do is the mock client's `Do` func
|
||||
func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
|
||||
return GetGetVaultsFunc()
|
||||
func (tc *TestClient) GetItemByID(ctx context.Context, vaultID, itemID string) (*model.Item, error) {
|
||||
args := tc.Called(vaultID, itemID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*model.Item), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
|
||||
return DoGetVaultsByTitleFunc(title)
|
||||
func (tc *TestClient) GetItemsByTitle(ctx context.Context, vaultID, itemTitle string) ([]model.Item, error) {
|
||||
args := tc.Called(vaultID, itemTitle)
|
||||
return args.Get(0).([]model.Item), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetVault(uuid string) (*onepassword.Vault, error) {
|
||||
return DoGetVaultFunc(uuid)
|
||||
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) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
return GetGetItemFunc(uuid, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
|
||||
return DoGetItemsFunc(vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItemsByTitle(title, vaultUUID string) ([]onepassword.Item, error) {
|
||||
return DoGetItemsByTitleFunc(title, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoGetItemByTitleFunc(title, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoCreateItemFunc(item, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error {
|
||||
return DoDeleteItemFunc(item, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoUpdateItemFunc(item, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) {
|
||||
return DoGetFileFunc(uuid, itemUUID, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) {
|
||||
return DoGetFileContentFunc(file)
|
||||
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)
|
||||
}
|
||||
|
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")
|
||||
}
|
84
pkg/onepassword/client/connect/connect.go
Normal file
84
pkg/onepassword/client/connect/connect.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package connect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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("1Password Connect error: %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("1Password Connect error: %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
|
||||
}
|
||||
|
||||
func (c *Connect) GetFileContent(ctx context.Context, vaultID, itemID, fileID string) ([]byte, error) {
|
||||
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 nil, fmt.Errorf("1Password Connect error: %w", err)
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
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("1Password Connect error: %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("1Password sdk error: %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("1Password sdk error: %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("1Password sdk error: %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("1Password sdk error: %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 (
|
||||
sdk "github.com/1password/onepassword-sdk-go"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
130
pkg/onepassword/client/testing/mock/connect.go
Normal file
130
pkg/onepassword/client/testing/mock/connect.go
Normal file
@@ -0,0 +1,130 @@
|
||||
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")
|
||||
}
|
89
pkg/onepassword/client/testing/mock/sdk.go
Normal file
89
pkg/onepassword/client/testing/mock/sdk.go
Normal file
@@ -0,0 +1,89 @@
|
||||
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)
|
||||
}
|
@@ -2,12 +2,12 @@ package onepassword
|
||||
|
||||
import (
|
||||
"context"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"os"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
errors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -15,16 +15,16 @@ import (
|
||||
)
|
||||
|
||||
var logConnectSetup = logf.Log.WithName("ConnectSetup")
|
||||
var deploymentPath = "deploy/connect/deployment.yaml"
|
||||
var servicePath = "deploy/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,27 @@ 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 +78,21 @@ 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)
|
||||
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 +108,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
|
||||
@@ -23,9 +24,9 @@ func TestServiceSetup(t *testing.T) {
|
||||
objs := []runtime.Object{}
|
||||
|
||||
// Create a fake client to mock API calls.
|
||||
client := fake.NewFakeClientWithScheme(s, objs...)
|
||||
client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
|
||||
|
||||
err := setupService(client, "../../deploy/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
|
||||
@@ -48,9 +50,9 @@ func TestDeploymentSetup(t *testing.T) {
|
||||
objs := []runtime.Object{}
|
||||
|
||||
// Create a fake client to mock API calls.
|
||||
client := fake.NewFakeClientWithScheme(s, objs...)
|
||||
client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
|
||||
|
||||
err := setupDeployment(client, "../../deploy/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)
|
||||
}
|
||||
|
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
func TestAreContainersUsingSecretsFromEnv(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
|
@@ -9,18 +9,30 @@ import (
|
||||
|
||||
func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
"onepassword-app-token": {},
|
||||
"onepassword-user-credentials": {},
|
||||
}
|
||||
|
||||
volumeSecretNames := []string{
|
||||
"onepassword-database-secret",
|
||||
"onepassword-api-key",
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
volumes := generateVolumes(volumeSecretNames)
|
||||
|
||||
volumeProjectedSecretNames := []string{
|
||||
"onepassword-app-token",
|
||||
"onepassword-user-credentials",
|
||||
}
|
||||
|
||||
volumeProjected := generateVolumesProjected(volumeProjectedSecretNames)
|
||||
|
||||
volumes = append(volumes, volumeProjected)
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
deployment.Spec.Template.Spec.Volumes = generateVolumes(volumeSecretNames)
|
||||
deployment.Spec.Template.Spec.Volumes = volumes
|
||||
if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) {
|
||||
t.Errorf("Expected that deployment was using secrets but they were not detected.")
|
||||
}
|
||||
@@ -28,8 +40,8 @@ func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) {
|
||||
|
||||
func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
@@ -47,8 +59,8 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) {
|
||||
|
||||
func TestIsDeploymentNotUSingSecrets(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
|
@@ -1,38 +1,40 @@
|
||||
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)
|
||||
_, err := opClient.GetFileContent(ctx, vaultID, itemID, file.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,15 +51,15 @@ func ParseVaultAndItemFromPath(path string) (string, string, error) {
|
||||
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
|
||||
}
|
||||
|
||||
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]
|
||||
@@ -67,22 +69,22 @@ 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]
|
||||
@@ -92,9 +94,9 @@ 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
|
||||
}
|
85
pkg/onepassword/model/item.go
Normal file
85
pkg/onepassword/model/item.go
Normal file
@@ -0,0 +1,85 @@
|
||||
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
|
||||
|
||||
for _, tag := range item.Tags {
|
||||
i.Tags = append(i.Tags, tag)
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
@@ -17,6 +17,29 @@ func generateVolumes(names []string) []corev1.Volume {
|
||||
}
|
||||
return volumes
|
||||
}
|
||||
func generateVolumesProjected(names []string) corev1.Volume {
|
||||
volumesProjection := []corev1.VolumeProjection{}
|
||||
for i := 0; i < len(names); i++ {
|
||||
volumeProjection := corev1.VolumeProjection{
|
||||
Secret: &corev1.SecretProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: names[i],
|
||||
},
|
||||
},
|
||||
}
|
||||
volumesProjection = append(volumesProjection, volumeProjection)
|
||||
}
|
||||
volume := corev1.Volume{
|
||||
Name: "someName",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Projected: &corev1.ProjectedVolumeSource{
|
||||
Sources: volumesProjection,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return volume
|
||||
}
|
||||
func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container {
|
||||
containers := []corev1.Container{}
|
||||
for i := 0; i < len(names); i++ {
|
||||
|
@@ -5,13 +5,13 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
v1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
|
||||
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"
|
||||
@@ -23,37 +23,37 @@ 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 +63,7 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
|
||||
return nil
|
||||
}
|
||||
|
||||
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap()
|
||||
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,31 +78,32 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
|
||||
}
|
||||
for _, secret := range updatedDeploymentSecrets {
|
||||
if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) {
|
||||
h.restartDeployment(deployment)
|
||||
h.restartDeployment(ctx, deployment)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.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) {
|
||||
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))
|
||||
deployment.Spec.Template.Annotations = map[string]string{
|
||||
RestartAnnotation: time.Now().String(),
|
||||
if deployment.Spec.Template.Annotations == nil {
|
||||
deployment.Spec.Template.Annotations = map[string]string{}
|
||||
}
|
||||
err := h.client.Update(context.Background(), deployment)
|
||||
deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String()
|
||||
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
|
||||
@@ -120,29 +121,35 @@ 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)
|
||||
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.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
|
||||
h.client.Update(context.Background(), &secret)
|
||||
if err := h.client.Update(ctx, &secret); err != nil {
|
||||
log.Error(err, "failed to update secret %s annotations to version %d: %s", secret.Name, itemVersion, err)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
|
||||
secret.Annotations[VersionAnnotation] = itemVersion
|
||||
secret.Annotations[ItemPathAnnotation] = itemPathString
|
||||
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, string(secret.Type), *item, nil)
|
||||
log.Info(fmt.Sprintf("New secret path: %v and version: %v", updatedSecret.Annotations[ItemPathAnnotation], updatedSecret.Annotations[VersionAnnotation]))
|
||||
h.client.Update(context.Background(), updatedSecret)
|
||||
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(ctx, &secret); err != nil {
|
||||
log.Error(err, "failed to update secret %s to version %d: %s", secret.Name, itemVersion, err)
|
||||
continue
|
||||
}
|
||||
if updatedSecrets[secret.Namespace] == nil {
|
||||
updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)
|
||||
}
|
||||
@@ -152,7 +159,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 {
|
||||
@@ -170,9 +177,9 @@ func isUpdatedSecret(secretName string, updatedSecrets map[string]*corev1.Secret
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
@@ -187,7 +194,7 @@ func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string {
|
||||
onePasswordItem := &v1.OnePasswordItem{}
|
||||
onePasswordItem := &onepasswordv1.OnePasswordItem{}
|
||||
|
||||
// Search for our original OnePasswordItem if it exists
|
||||
err := h.client.Get(context.TODO(), client.ObjectKey{
|
||||
|
@@ -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"
|
||||
@@ -122,6 +125,9 @@ var tests = []testUpdateSecretTask{
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
@@ -235,6 +241,9 @@ var tests = []testUpdateSecretTask{
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
@@ -342,6 +351,9 @@ var tests = []testUpdateSecretTask{
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
@@ -411,6 +423,9 @@ var tests = []testUpdateSecretTask{
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
@@ -482,6 +497,9 @@ var tests = []testUpdateSecretTask{
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
@@ -553,6 +571,9 @@ var tests = []testUpdateSecretTask{
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
@@ -630,6 +651,9 @@ var tests = []testUpdateSecretTask{
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
@@ -703,6 +727,9 @@ var tests = []testUpdateSecretTask{
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
@@ -760,7 +787,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)
|
||||
@@ -776,25 +803,17 @@ func TestUpdateSecretHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create a fake client to mock API calls.
|
||||
cl := fake.NewFakeClientWithScheme(s, objs...)
|
||||
cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
|
||||
|
||||
opConnectClient := &mocks.TestClient{}
|
||||
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
|
||||
item.Version = 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)
|
||||
|
||||
@@ -807,7 +826,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)
|
||||
@@ -821,7 +840,7 @@ func TestUpdateSecretHandler(t *testing.T) {
|
||||
|
||||
//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)
|
||||
|
||||
_, ok := deployment.Spec.Template.Annotations[RestartAnnotation]
|
||||
if ok {
|
||||
@@ -829,6 +848,16 @@ func TestUpdateSecretHandler(t *testing.T) {
|
||||
} else {
|
||||
assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.")
|
||||
}
|
||||
|
||||
oldPodTemplateAnnotations := testData.existingDeployment.Spec.Template.ObjectMeta.Annotations
|
||||
newPodTemplateAnnotations := deployment.Spec.Template.Annotations
|
||||
for name, expected := range oldPodTemplateAnnotations {
|
||||
actual, ok := newPodTemplateAnnotations[name]
|
||||
if assert.Truef(t, ok, "Annotation %s was present in original pod template but was dropped after update", name) {
|
||||
assert.Equalf(t, expected, actual, "Annotation value for %s original pod template has changed", name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -837,7 +866,7 @@ func TestIsUpdatedSecret(t *testing.T) {
|
||||
|
||||
secretName := "test-secret"
|
||||
updatedSecrets := map[string]*corev1.Secret{
|
||||
"some_secret": &corev1.Secret{},
|
||||
"some_secret": {},
|
||||
}
|
||||
assert.False(t, isUpdatedSecret(secretName, updatedSecrets))
|
||||
|
||||
@@ -845,16 +874,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
|
||||
}
|
||||
|
@@ -4,26 +4,55 @@ import corev1 "k8s.io/api/core/v1"
|
||||
|
||||
func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret) bool {
|
||||
for i := 0; i < len(volumes); i++ {
|
||||
if secret := volumes[i].Secret; secret != nil {
|
||||
secretName := secret.SecretName
|
||||
_, ok := secrets[secretName]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
secret := IsVolumeUsingSecret(volumes[i], secrets)
|
||||
secretProjection := IsVolumeUsingSecretProjection(volumes[i], secrets)
|
||||
if secret == nil && secretProjection == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
if len(volumes) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func AppendUpdatedVolumeSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret {
|
||||
for i := 0; i < len(volumes); i++ {
|
||||
if secret := volumes[i].Secret; secret != nil {
|
||||
secretName := secret.SecretName
|
||||
secret, ok := secrets[secretName]
|
||||
if ok {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
secret := IsVolumeUsingSecret(volumes[i], secrets)
|
||||
if secret != nil {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
} else {
|
||||
secretProjection := IsVolumeUsingSecretProjection(volumes[i], secrets)
|
||||
if secretProjection != nil {
|
||||
updatedDeploymentSecrets[secretProjection.Name] = secretProjection
|
||||
}
|
||||
}
|
||||
}
|
||||
return updatedDeploymentSecrets
|
||||
}
|
||||
|
||||
func IsVolumeUsingSecret(volume corev1.Volume, secrets map[string]*corev1.Secret) *corev1.Secret {
|
||||
if secret := volume.Secret; secret != nil {
|
||||
secretName := secret.SecretName
|
||||
secretFound, ok := secrets[secretName]
|
||||
if ok {
|
||||
return secretFound
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsVolumeUsingSecretProjection(volume corev1.Volume, secrets map[string]*corev1.Secret) *corev1.Secret {
|
||||
if volume.Projected != nil {
|
||||
for i := 0; i < len(volume.Projected.Sources); i++ {
|
||||
if secret := volume.Projected.Sources[i].Secret; secret != nil {
|
||||
secretName := secret.Name
|
||||
secretFound, ok := secrets[secretName]
|
||||
if ok {
|
||||
return secretFound
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@@ -8,18 +8,28 @@ import (
|
||||
|
||||
func TestAreVolmesUsingSecrets(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
"onepassword-app-token": {},
|
||||
"onepassword-user-credentials": {},
|
||||
}
|
||||
|
||||
volumeSecretNames := []string{
|
||||
"onepassword-database-secret",
|
||||
"onepassword-api-key",
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
volumes := generateVolumes(volumeSecretNames)
|
||||
|
||||
volumeProjectedSecretNames := []string{
|
||||
"onepassword-app-token",
|
||||
"onepassword-user-credentials",
|
||||
}
|
||||
|
||||
volumeProjected := generateVolumesProjected(volumeProjectedSecretNames)
|
||||
|
||||
volumes = append(volumes, volumeProjected)
|
||||
|
||||
if !AreVolumesUsingSecrets(volumes, secretNamesToSearch) {
|
||||
t.Errorf("Expected that volumes were using secrets but they were not detected.")
|
||||
}
|
||||
@@ -27,8 +37,8 @@ func TestAreVolmesUsingSecrets(t *testing.T) {
|
||||
|
||||
func TestAreVolumesNotUsingSecrets(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
}
|
||||
|
||||
volumeSecretNames := []string{
|
||||
|
@@ -1,10 +1,26 @@
|
||||
// Copyright 2018 The Operator-SDK Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/logs"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
var ForceRunModeEnv = "OSDK_FORCE_RUN_MODE"
|
||||
@@ -16,6 +32,8 @@ const (
|
||||
ClusterRunMode RunModeType = "cluster"
|
||||
)
|
||||
|
||||
var log = logf.Log.WithName("k8sutil")
|
||||
|
||||
// ErrNoNamespace indicates that a namespace could not be found for the current
|
||||
// environment
|
||||
var ErrNoNamespace = fmt.Errorf("namespace not found for current environment")
|
||||
@@ -29,7 +47,7 @@ func GetOperatorNamespace() (string, error) {
|
||||
if isRunModeLocal() {
|
||||
return "", ErrRunLocal
|
||||
}
|
||||
nsBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
nsBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", ErrNoNamespace
|
||||
@@ -37,6 +55,7 @@ func GetOperatorNamespace() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
ns := strings.TrimSpace(string(nsBytes))
|
||||
log.V(logs.DebugLevel).Info("Found namespace", "Namespace", ns)
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package version
|
||||
|
||||
// TODO figure out if this package makes sense.
|
||||
var (
|
||||
Version = "0.0.1"
|
||||
OperatorVersion = "1.9.0"
|
||||
OperatorSDKVersion = "1.34.1"
|
||||
)
|
||||
|
Reference in New Issue
Block a user