mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 07:28:06 +00:00
Compare commits
159 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
0796b9c5e2 | ||
![]() |
37a0f4b51e | ||
![]() |
004e0101ff | ||
![]() |
6326a856ae | ||
![]() |
1ddf92c5a0 | ||
![]() |
f5c6fa5860 | ||
![]() |
afa076d321 | ||
![]() |
d4b04c233c | ||
![]() |
ea68cfc2b4 | ||
![]() |
58b4ff8ecf | ||
![]() |
d93fecdc76 | ||
![]() |
486465247d | ||
![]() |
79868ae374 | ||
![]() |
6286f7e306 | ||
![]() |
0b5efc8690 | ||
![]() |
c00baeedcb | ||
![]() |
a37bddbfd9 | ||
![]() |
bd9922f635 | ||
![]() |
8fa4413880 | ||
![]() |
62e55a3f19 | ||
![]() |
d6f7b80c40 | ||
![]() |
a903f9b1af | ||
![]() |
5cddc9d8a9 | ||
![]() |
7e1b94fae7 | ||
![]() |
6953a89c89 | ||
![]() |
0d9e07f543 | ||
![]() |
098d504d2a | ||
![]() |
b68d9a5d79 | ||
![]() |
befcaae457 | ||
![]() |
b24aa48bd6 | ||
![]() |
b1e251dee6 | ||
![]() |
a34c6e8b38 | ||
![]() |
b16960057a | ||
![]() |
285496dc7e | ||
![]() |
f38cf7e1c2 | ||
![]() |
bb7a0c8ca9 | ||
![]() |
302653832e | ||
![]() |
a1bcfdfdcb | ||
![]() |
c0f1632638 | ||
![]() |
c46065fa7a | ||
![]() |
5d229c42d5 | ||
![]() |
c7235b4f09 | ||
![]() |
5183fc129a | ||
![]() |
7d619165b2 | ||
![]() |
0363ae1e4e | ||
![]() |
d9e003bdb7 | ||
![]() |
b25f943b3a | ||
![]() |
5fab662424 | ||
![]() |
d807e92c36 | ||
![]() |
244771717c | ||
![]() |
a760e524ea | ||
![]() |
7aeb36e383 | ||
![]() |
5c2f840623 | ||
![]() |
670040477e | ||
![]() |
a45a310611 | ||
![]() |
d80e8dd799 | ||
![]() |
88728909ff | ||
![]() |
e365ebfdfa | ||
![]() |
2c4b4df01a | ||
![]() |
19f774bb2d | ||
![]() |
0193a98681 | ||
![]() |
f241d7423d | ||
![]() |
c0037526b0 | ||
![]() |
dff934cbc3 | ||
![]() |
2096f4440f | ||
![]() |
b3fc707337 | ||
![]() |
32643651d9 | ||
![]() |
ba8d3fa698 | ||
![]() |
c57aa22a9c | ||
![]() |
48944b0d56 | ||
![]() |
313cd1169b | ||
![]() |
fb1262f1bd | ||
![]() |
a428fe7462 | ||
![]() |
ea2d1f8a09 | ||
![]() |
bd96d50a9b |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
# Ignore build and test binaries.
|
||||
bin/
|
||||
testbin/
|
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.20
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./... -cover
|
||||
run: make test
|
||||
|
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@v6
|
||||
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@v3
|
||||
|
||||
- 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@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Docker meta
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: crazy-max/ghaction-docker-meta@v2
|
||||
uses: docker/metadata-action@v4
|
||||
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@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@v2
|
||||
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@v3
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
|
87
.gitignore
vendored
87
.gitignore
vendored
@@ -1,80 +1,25 @@
|
||||
# Temporary Build Files
|
||||
build/_output
|
||||
build/_test
|
||||
# Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode
|
||||
### Emacs ###
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
# elpa packages
|
||||
/elpa/
|
||||
# reftex files
|
||||
*.rel
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
# server auth directory
|
||||
/server/
|
||||
# projectiles files
|
||||
.projectile
|
||||
projectile-bookmarks.eld
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
# saveplace
|
||||
places
|
||||
# url cache
|
||||
url/cache/
|
||||
# cedet
|
||||
ede-projects.el
|
||||
# smex
|
||||
smex-items
|
||||
# company-statistics
|
||||
company-statistics-cache.el
|
||||
# anaconda-mode
|
||||
anaconda-mode/
|
||||
### Go ###
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
# Test binary, build with 'go test -c'
|
||||
bin
|
||||
testbin/*
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
### Vim ###
|
||||
# swap
|
||||
.sw[a-p]
|
||||
.*.sw[a-p]
|
||||
# session
|
||||
Session.vim
|
||||
# temporary
|
||||
.netrwhist
|
||||
# auto-generated tag files
|
||||
tags
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
.history
|
||||
.DS_Store
|
||||
op-ss-client/
|
||||
.idea/
|
||||
# End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode
|
||||
|
||||
# Kubernetes Generated files - skip generated files, except for vendored files
|
||||
|
||||
!vendor/**/zz_generated.*
|
||||
|
||||
# editor and IDE paraphernalia
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
152
CHANGELOG.md
152
CHANGELOG.md
@@ -12,60 +12,164 @@
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.0.2)
|
||||
[//]: # (START/v1.7.0)
|
||||
# v1.7.0
|
||||
|
||||
## Features
|
||||
* Upgraded operator to version 1.29.0. {#162}
|
||||
* Upgraded Golang version to 1.20. {#161}
|
||||
* Upgraded 1Password Connect version to 1.5.1. {#161}
|
||||
* Added runAsNonRoot and allowPrivalegeEscalation to specs. {#151}
|
||||
* Added code quality improvements. {#146}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.6.0)
|
||||
# v1.6.0
|
||||
|
||||
This version of the operator highlights the migration of the operator
|
||||
to use the latest version of the `operator-sdk` (`1.25.0` at the time of this release).
|
||||
|
||||
For the users, this shouldn't affect the functionality of the operator.
|
||||
|
||||
This migration enables us to use the new project structure, as well as updated packages that enables
|
||||
the team (as well as the contributors) to develop the operator more effective.
|
||||
|
||||
## Features
|
||||
* Migrate the operator to use the latest `operator-sdk` {#124}
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.5.0)
|
||||
# v1.5.0
|
||||
|
||||
## Features
|
||||
* `OnePasswordItem` now contains a `status` which contains the status of creating the kubernetes secret for a OnePasswordItem. {#52}
|
||||
|
||||
## Fixes
|
||||
* The operator no longer logs an error about changing the secret type if the secret type is not actually being changed.
|
||||
* Annotations on a deployment are no longer removed when the operator triggers a restart. {#112}
|
||||
|
||||
---
|
||||
|
||||
[//]: # "START/v1.4.1"
|
||||
|
||||
# v1.4.1
|
||||
|
||||
## Fixes
|
||||
|
||||
- OwnerReferences on secrets are now persisted after an item is updated. {#101}
|
||||
- Annotations from a Deployment or OnePasswordItem are no longer applied to Secrets that are created for it. {#102}
|
||||
|
||||
---
|
||||
|
||||
[//]: # "START/v1.4.0"
|
||||
|
||||
# v1.4.0
|
||||
|
||||
## Features
|
||||
|
||||
- The operator now declares the an OwnerReference for the secrets it manages. This should stop secrets from getting pruned by tools like Argo CD. {#51,#84,#96}
|
||||
|
||||
---
|
||||
|
||||
[//]: # "START/v1.3.0"
|
||||
|
||||
# v1.3.0
|
||||
|
||||
## Features
|
||||
|
||||
- Added support for loading secrets from files stored in 1Password. {#47}
|
||||
|
||||
---
|
||||
|
||||
[//]: # "START/v1.2.0"
|
||||
|
||||
# v1.2.0
|
||||
|
||||
## Features
|
||||
|
||||
- Support secrets provisioned through FromEnv. {#74}
|
||||
- Support configuration of Kubernetes Secret type. {#87}
|
||||
- Improved logging. (#72)
|
||||
|
||||
---
|
||||
|
||||
[//]: # "START/v1.1.0"
|
||||
|
||||
# v1.1.0
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
|
||||
|
||||
---
|
||||
|
||||
[//]: # "START/v1.0.2"
|
||||
|
||||
# v1.0.2
|
||||
|
||||
## Fixes
|
||||
* Name normalizer added to handle non-conforming item names.
|
||||
|
||||
- Name normalizer added to handle non-conforming item names.
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.0.1)
|
||||
[//]: # "START/v1.0.1"
|
||||
|
||||
# v1.0.1
|
||||
|
||||
## Features
|
||||
* This release also contains an arm64 Docker image. {#20}
|
||||
* Docker images are also pushed to the :latest and :<major>.<minor> tags.
|
||||
|
||||
- This release also contains an arm64 Docker image. {#20}
|
||||
- Docker images are also pushed to the :latest and :<major>.<minor> tags.
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v1.0.0)
|
||||
[//]: # "START/v1.0.0"
|
||||
|
||||
# v1.0.0
|
||||
|
||||
## Features:
|
||||
* Option to automatically deploy 1Password Connect via the operator
|
||||
* Ignore restart annotation when looking for 1Password annotations
|
||||
* Release Automation
|
||||
* Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource
|
||||
* Adding configuration for auto rolling restart on deployments
|
||||
* Configure Auto Restarts for a OnePasswordItem Custom Resource
|
||||
* Update Connect Dependencies to latest
|
||||
* Add Github action for building and testing operator
|
||||
|
||||
- Option to automatically deploy 1Password Connect via the operator
|
||||
- Ignore restart annotation when looking for 1Password annotations
|
||||
- Release Automation
|
||||
- Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource
|
||||
- Adding configuration for auto rolling restart on deployments
|
||||
- Configure Auto Restarts for a OnePasswordItem Custom Resource
|
||||
- Update Connect Dependencies to latest
|
||||
- Add Github action for building and testing operator
|
||||
|
||||
## Fixes:
|
||||
* Fix spec field example for OnePasswordItem in readme
|
||||
* Casing of annotations are now consistent
|
||||
|
||||
- Fix spec field example for OnePasswordItem in readme
|
||||
- Casing of annotations are now consistent
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v0.0.2)
|
||||
[//]: # "START/v0.0.2"
|
||||
|
||||
# v0.0.2
|
||||
|
||||
## Features:
|
||||
* Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>`
|
||||
|
||||
- Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>`
|
||||
|
||||
---
|
||||
|
||||
[//]: # (START/v0.0.1)
|
||||
[//]: # "START/v0.0.1"
|
||||
|
||||
# v0.0.1
|
||||
|
||||
Initial 1Password Operator release
|
||||
|
||||
## Features
|
||||
* watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret
|
||||
* watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets
|
||||
* watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes
|
||||
* restart pods when secret has been updated
|
||||
* cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted
|
||||
|
||||
- watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret
|
||||
- watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets
|
||||
- watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes
|
||||
- restart pods when secret has been updated
|
||||
- cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted
|
||||
|
||||
---
|
||||
|
23
Dockerfile
23
Dockerfile
@@ -1,20 +1,31 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.13 as builder
|
||||
FROM golang:1.20 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
|
||||
RUN go mod download
|
||||
|
||||
# Copy the go source
|
||||
COPY cmd/manager/main.go main.go
|
||||
COPY main.go main.go
|
||||
COPY api/ api/
|
||||
COPY controllers/ controllers/
|
||||
COPY pkg/ pkg/
|
||||
COPY version/ version/
|
||||
COPY vendor/ vendor/
|
||||
|
||||
# Build
|
||||
ARG operator_version=dev
|
||||
# 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 \
|
||||
GO111MODULE=on \
|
||||
GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
|
||||
go build \
|
||||
-ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \
|
||||
-mod vendor \
|
||||
@@ -25,7 +36,7 @@ RUN CGO_ENABLED=0 \
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/manager .
|
||||
USER nonroot:nonroot
|
||||
COPY deploy/connect/ deploy/connect/
|
||||
USER 65532:65532
|
||||
COPY config/connect/ config/connect/
|
||||
|
||||
ENTRYPOINT ["/manager"]
|
||||
|
293
Makefile
293
Makefile
@@ -1,8 +1,266 @@
|
||||
export MAIN_BRANCH ?= main
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: test build build/binary build/local clean test/coverage release/prepare release/tag .check_bump_type .check_git_clean help
|
||||
# VERSION defines the project version for the bundle.
|
||||
# Update this value when you upgrade the version of your project.
|
||||
# To re-generate a bundle for another specific version without changing the standard setup, you can:
|
||||
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
|
||||
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
|
||||
VERSION ?= 1.6.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")
|
||||
# To re-generate a bundle for other specific channels without changing the standard setup, you can:
|
||||
# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable)
|
||||
# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable")
|
||||
ifneq ($(origin CHANNELS), undefined)
|
||||
BUNDLE_CHANNELS := --channels=$(CHANNELS)
|
||||
endif
|
||||
|
||||
# DEFAULT_CHANNEL defines the default channel used in the bundle.
|
||||
# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable")
|
||||
# To re-generate a bundle for any other default channel without changing the default setup, you can:
|
||||
# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable)
|
||||
# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable")
|
||||
ifneq ($(origin DEFAULT_CHANNEL), undefined)
|
||||
BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
|
||||
endif
|
||||
BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
|
||||
|
||||
# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images.
|
||||
# This variable is used to construct full image tags for bundle and catalog images.
|
||||
#
|
||||
# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
|
||||
# onepassword.com/onepassword-operator-bundle:$VERSION and onepassword.com/onepassword-operator-catalog:$VERSION.
|
||||
IMAGE_TAG_BASE ?= onepassword.com/onepassword-operator
|
||||
|
||||
# BUNDLE_IMG defines the image:tag used for the bundle.
|
||||
# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
|
||||
BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)
|
||||
|
||||
# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command
|
||||
BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
|
||||
|
||||
# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests
|
||||
# You can enable this value if you would like to use SHA Based Digests
|
||||
# To enable set flag to true
|
||||
USE_IMAGE_DIGESTS ?= false
|
||||
ifeq ($(USE_IMAGE_DIGESTS), true)
|
||||
BUNDLE_GEN_FLAGS += --use-image-digests
|
||||
endif
|
||||
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= 1password/onepassword-operator:latest
|
||||
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
||||
ENVTEST_K8S_VERSION = 1.26
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
GOBIN=$(shell go env GOPATH)/bin
|
||||
else
|
||||
GOBIN=$(shell go env GOBIN)
|
||||
endif
|
||||
|
||||
# Setting SHELL to bash allows bash commands to be executed by recipes.
|
||||
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
|
||||
SHELL = /usr/bin/env bash -o pipefail
|
||||
.SHELLFLAGS = -ec
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
##@ General
|
||||
|
||||
# The help target prints out all targets with their descriptions organized
|
||||
# beneath their categories. The categories are represented by '##@' and the
|
||||
# target descriptions by '##'. The awk commands is responsible for reading the
|
||||
# entire set of makefiles included in this invocation, looking for lines of the
|
||||
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||
# More info on the usage of ANSI control characters for terminal formatting:
|
||||
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||
# More info on the awk command:
|
||||
# http://linuxcommand.org/lc3_adv_awk.php
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Development
|
||||
|
||||
.PHONY: manifests
|
||||
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
|
||||
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
|
||||
|
||||
.PHONY: generate
|
||||
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
|
||||
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## Run go fmt against code.
|
||||
go fmt ./...
|
||||
|
||||
.PHONY: vet
|
||||
vet: ## Run go vet against code.
|
||||
go vet ./...
|
||||
|
||||
.PHONY: test
|
||||
test: manifests generate fmt vet envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
|
||||
|
||||
##@ Build
|
||||
|
||||
.PHONY: build
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager main.go
|
||||
|
||||
.PHONY: run
|
||||
run: manifests generate fmt vet ## Run a controller from your host.
|
||||
go run ./main.go
|
||||
|
||||
# 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} .
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: ## Push docker image with the manager.
|
||||
docker 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>> than 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: test ## Build and push docker image for the manager for cross-platform support
|
||||
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
|
||||
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
|
||||
- docker buildx create --name project-v3-builder
|
||||
docker buildx use project-v3-builder
|
||||
- docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross
|
||||
- docker buildx rm project-v3-builder
|
||||
rm Dockerfile.cross
|
||||
|
||||
##@ Deployment
|
||||
|
||||
ifndef ignore-not-found
|
||||
ignore-not-found = false
|
||||
endif
|
||||
|
||||
.PHONY: install
|
||||
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
|
||||
$(KUSTOMIZE) build config/crd | kubectl apply -f -
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
|
||||
.PHONY: set-namespace
|
||||
set-namespace:
|
||||
cd config/default && $(KUSTOMIZE) edit set namespace $(shell kubectl config view --minify -o jsonpath={..namespace})
|
||||
|
||||
.PHONY: deploy
|
||||
deploy: manifests kustomize set-namespace ## Deploy controller to the K8s cluster specified in ~/.kube/config.
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
|
||||
$(KUSTOMIZE) build config/default | kubectl apply -f -
|
||||
|
||||
.PHONY: undeploy
|
||||
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
$(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
|
||||
##@ Build Dependencies
|
||||
|
||||
## Location to install dependencies to
|
||||
LOCALBIN ?= $(shell pwd)/bin
|
||||
$(LOCALBIN):
|
||||
mkdir -p $(LOCALBIN)
|
||||
|
||||
## Tool Binaries
|
||||
KUSTOMIZE ?= $(LOCALBIN)/kustomize
|
||||
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
|
||||
ENVTEST ?= $(LOCALBIN)/setup-envtest
|
||||
|
||||
## Tool Versions
|
||||
KUSTOMIZE_VERSION ?= v4.5.7
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.10.0
|
||||
|
||||
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
|
||||
.PHONY: kustomize
|
||||
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
|
||||
$(KUSTOMIZE): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/kustomize || { curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); }
|
||||
|
||||
.PHONY: controller-gen
|
||||
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
|
||||
$(CONTROLLER_GEN): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/controller-gen || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
|
||||
|
||||
.PHONY: envtest
|
||||
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
|
||||
|
||||
.PHONY: bundle
|
||||
bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files.
|
||||
operator-sdk generate kustomize manifests -q
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG)
|
||||
$(KUSTOMIZE) build config/manifests | operator-sdk generate bundle $(BUNDLE_GEN_FLAGS)
|
||||
operator-sdk bundle validate ./bundle
|
||||
|
||||
.PHONY: bundle-build
|
||||
bundle-build: ## Build the bundle image.
|
||||
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
|
||||
|
||||
.PHONY: bundle-push
|
||||
bundle-push: ## Push the bundle image.
|
||||
$(MAKE) docker-push IMG=$(BUNDLE_IMG)
|
||||
|
||||
.PHONY: opm
|
||||
OPM = ./bin/opm
|
||||
opm: ## Download opm locally if necessary.
|
||||
ifeq (,$(wildcard $(OPM)))
|
||||
ifeq (,$(shell which opm 2>/dev/null))
|
||||
@{ \
|
||||
set -e ;\
|
||||
mkdir -p $(dir $(OPM)) ;\
|
||||
OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
|
||||
curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\
|
||||
chmod +x $(OPM) ;\
|
||||
}
|
||||
else
|
||||
OPM = $(shell which opm)
|
||||
endif
|
||||
endif
|
||||
|
||||
# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0).
|
||||
# These images MUST exist in a registry and be pull-able.
|
||||
BUNDLE_IMGS ?= $(BUNDLE_IMG)
|
||||
|
||||
# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0).
|
||||
CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION)
|
||||
|
||||
# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image.
|
||||
ifneq ($(origin CATALOG_BASE_IMG), undefined)
|
||||
FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG)
|
||||
endif
|
||||
|
||||
# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'.
|
||||
# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see:
|
||||
# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
|
||||
.PHONY: catalog-build
|
||||
catalog-build: opm ## Build a catalog image.
|
||||
$(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
|
||||
|
||||
# Push the catalog image.
|
||||
.PHONY: catalog-push
|
||||
catalog-push: ## Push a catalog image.
|
||||
$(MAKE) docker-push IMG=$(CATALOG_IMG)
|
||||
|
||||
## Release functions =====================
|
||||
GIT_BRANCH := $(shell git symbolic-ref --short HEAD)
|
||||
WORKTREE_CLEAN := $(shell git status --porcelain 1>/dev/null 2>&1; echo $$?)
|
||||
SCRIPTS_DIR := $(CURDIR)/scripts
|
||||
@@ -10,37 +268,6 @@ SCRIPTS_DIR := $(CURDIR)/scripts
|
||||
versionFile = $(CURDIR)/.VERSION
|
||||
curVersion := $(shell cat $(versionFile) | sed 's/^v//')
|
||||
|
||||
OPERATOR_NAME := onepassword-connect-operator
|
||||
DOCKER_IMG_TAG ?= $(OPERATOR_NAME):v$(curVersion)
|
||||
|
||||
test: ## Run test suite
|
||||
go test ./...
|
||||
|
||||
test/coverage: ## Run test suite with coverage report
|
||||
go test -v ./... -cover
|
||||
|
||||
build: ## Build operator Docker image
|
||||
@docker build -f Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG) .
|
||||
@echo "Successfully built and tagged image."
|
||||
@echo "Tag: $(DOCKER_IMG_TAG)"
|
||||
|
||||
build/local: ## Build local version of the operator Docker image
|
||||
@docker build -f Dockerfile -t local/$(DOCKER_IMG_TAG) .
|
||||
|
||||
build/binary: clean ## Build operator binary
|
||||
@mkdir -p dist
|
||||
@go build -mod vendor -a -o manager ./cmd/manager/main.go
|
||||
@mv manager ./dist
|
||||
|
||||
clean:
|
||||
rm -rf ./dist
|
||||
|
||||
help: ## Prints this help message
|
||||
@grep -E '^[\/a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
|
||||
## Release functions =====================
|
||||
|
||||
release/prepare: .check_git_clean ## Updates changelog and creates release branch (call with 'release/prepare version=<new_version_number>')
|
||||
|
||||
@test $(version) || (echo "[ERROR] version argument not set."; exit 1)
|
||||
|
18
PROJECT
Normal file
18
PROJECT
Normal file
@@ -0,0 +1,18 @@
|
||||
domain: onepassword.com
|
||||
layout:
|
||||
- go.kubebuilder.io/v4-alpha
|
||||
plugins:
|
||||
manifests.sdk.operatorframework.io/v2: {}
|
||||
scorecard.sdk.operatorframework.io/v2: {}
|
||||
projectName: onepassword-operator
|
||||
repo: github.com/1Password/onepassword-operator
|
||||
resources:
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: onepassword.com
|
||||
kind: OnePasswordItem
|
||||
path: github.com/1Password/onepassword-operator/api/v1
|
||||
version: v1
|
||||
version: "3"
|
217
README.md
217
README.md
@@ -6,94 +6,100 @@ The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to
|
||||
|
||||
The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted.
|
||||
|
||||
## Setup
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Quickstart for Deploying 1Password Connect to Kubernetes](#quickstart-for-deploying-1password-connect-to-kubernetes)
|
||||
- [Kubernetes Operator Deployment](#kubernetes-operator-deployment)
|
||||
- [Usage](#usage)
|
||||
- [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
|
||||
- [Development](#development)
|
||||
- [Security](#security)
|
||||
|
||||
Prerequisites:
|
||||
## Prerequisites
|
||||
|
||||
- [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/)
|
||||
- [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
- [docker installed](https://docs.docker.com/get-docker/)
|
||||
- [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.
|
||||
- [`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://developer.1password.com/docs/connect/get-started/#step-1-set-up-a-secrets-automation-workflow)
|
||||
- [A `1password-credentials.json` file generated and a 1Password Connect API Token issues for the K8s Operator integration](https://developer.1password.com/docs/connect/get-started/#step-1-set-up-a-secrets-automation-workflow)
|
||||
## Quickstart for Deploying 1Password Connect to Kubernetes
|
||||
|
||||
### Quickstart for Deploying 1Password Connect to Kubernetes
|
||||
If 1Password Connect is already running, you can skip this step.
|
||||
|
||||
There are options to deploy 1Password Connect:
|
||||
|
||||
- [Deploy with Helm](#deploy-with-helm)
|
||||
- [Deploy using the Connect Operator](#deploy-using-the-connect-operator)
|
||||
|
||||
#### Deploy with Helm
|
||||
The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes.
|
||||
|
||||
The 1Password Connect Helm Chart 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:
|
||||
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 | \
|
||||
cat 1password-credentials.json | base64 | \
|
||||
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
|
||||
```
|
||||
|
||||
Create a Kubernetes secret from the op-session file:
|
||||
```bash
|
||||
|
||||
$ kubectl create secret generic op-credentials --from-file=1password-credentials.json
|
||||
```bash
|
||||
kubectl create secret generic op-credentials --from-file=op-session
|
||||
```
|
||||
|
||||
Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`:
|
||||
Add the following environment variable to the onepassword-connect-operator container in `/config/manager/manager.yaml`:
|
||||
|
||||
```yaml
|
||||
- name: MANAGE_CONNECT
|
||||
value: "true"
|
||||
```
|
||||
Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the `default` namespace.
|
||||
|
||||
Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the current namespace.
|
||||
|
||||
### Kubernetes Operator Deployment
|
||||
|
||||
**Create Kubernetes Secret for OP_CONNECT_TOKEN**
|
||||
|
||||
"Create a Connect token for the operator and save it as a Kubernetes Secret:
|
||||
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>"
|
||||
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.
|
||||
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 apply -f deploy/permissions.yaml
|
||||
```
|
||||
|
||||
**Create Custom One Password Secret Resource**
|
||||
|
||||
```bash
|
||||
$ kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
|
||||
kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
|
||||
```
|
||||
|
||||
**Deploying the Operator**
|
||||
|
||||
An sample Deployment yaml can be found at `/deploy/operator.yaml`.
|
||||
An sample Deployment yaml can be found at `/config/manager/manager.yaml`.
|
||||
|
||||
To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml:
|
||||
|
||||
To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml:
|
||||
|
||||
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
Apply the deployment file:
|
||||
To deploy the operator, simply run the following command:
|
||||
|
||||
```yaml
|
||||
kubectl apply -f deploy/operator.yaml
|
||||
```shell
|
||||
make deploy
|
||||
```
|
||||
|
||||
**Undeploy Operator**
|
||||
|
||||
```
|
||||
make undeploy
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -106,22 +112,22 @@ 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
|
||||
kubectl apply -f <your_item>.yaml
|
||||
```
|
||||
|
||||
To test that the Kubernetes Secret check that the following command returns a secret:
|
||||
|
||||
```bash
|
||||
$ kubectl get secret <secret_name>
|
||||
kubectl get secret <secret_name>
|
||||
```
|
||||
|
||||
Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.
|
||||
**Note:** Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.
|
||||
|
||||
To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata:
|
||||
|
||||
@@ -137,31 +143,42 @@ metadata:
|
||||
|
||||
Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Path.
|
||||
|
||||
Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret.
|
||||
The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password.
|
||||
In case of fields that store files, the file's contents will be used as the value.
|
||||
|
||||
Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence.
|
||||
|
||||
**Note:** Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret.
|
||||
|
||||
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
|
||||
|
||||
---
|
||||
|
||||
**NOTE**
|
||||
|
||||
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.
|
||||
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.
|
||||
|
||||
- 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
|
||||
## 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.
|
||||
There are many levels of granularity on which to configure auto restarts on deployments:
|
||||
- Operator level
|
||||
- Per-namespace
|
||||
- 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.
|
||||
**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
|
||||
@@ -171,10 +188,12 @@ metadata:
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
If the value is not set, the auto reset settings on the operator will be used. This value can be overwritten by deployment.
|
||||
|
||||
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
|
||||
@@ -184,10 +203,12 @@ metadata:
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
If the value is not set, the auto reset settings on the namespace will be used.
|
||||
|
||||
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
|
||||
@@ -197,39 +218,83 @@ metadata:
|
||||
annotations:
|
||||
operator.1password.io/auto-restart: "true"
|
||||
```
|
||||
If the value is not set, the auto reset settings on the deployment will be used.
|
||||
|
||||
If the value is not set, the auto restart settings on the deployment will be used.
|
||||
|
||||
<!--
|
||||
## Getting Started
|
||||
You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster.
|
||||
**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows).
|
||||
|
||||
### Running on the cluster
|
||||
1. Install Instances of Custom Resources:
|
||||
|
||||
```sh
|
||||
kubectl apply -f config/samples/
|
||||
```
|
||||
|
||||
2. Deploy the controller to the cluster with the image specified by `IMG`:
|
||||
|
||||
```sh
|
||||
make deploy IMG=<some-registry>/onepassword-operator:tag
|
||||
```
|
||||
|
||||
### Uninstall CRDs
|
||||
To delete the CRDs from the cluster:
|
||||
|
||||
```sh
|
||||
make uninstall
|
||||
```
|
||||
|
||||
### Undeploy controller
|
||||
UnDeploy the controller to the cluster:
|
||||
|
||||
```sh
|
||||
make undeploy
|
||||
```
|
||||
-->
|
||||
|
||||
## Development
|
||||
|
||||
### Creating a Docker image
|
||||
### How it works
|
||||
|
||||
To create a local version of the Docker image for testing, use the following `Makefile` target:
|
||||
```shell
|
||||
make build/local
|
||||
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
|
||||
```
|
||||
|
||||
### Building the Operator binary
|
||||
```shell
|
||||
make build/binary
|
||||
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
|
||||
```
|
||||
|
||||
The binary will be placed inside a `dist` folder within this repository.
|
||||
**NOTE:** You can also run this in one step by running: `make install run`
|
||||
|
||||
### Running Tests
|
||||
### Modifying the API definitions
|
||||
|
||||
```shell
|
||||
make test
|
||||
If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
|
||||
|
||||
```sh
|
||||
make manifests
|
||||
```
|
||||
|
||||
With coverage:
|
||||
```shell
|
||||
make test/coverage
|
||||
```
|
||||
**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)
|
||||
|
||||
## Security
|
||||
|
||||
1Password requests you practice responsible disclosure if you discover a vulnerability.
|
||||
1Password requests you practice responsible disclosure if you discover a vulnerability.
|
||||
|
||||
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
|
||||
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
|
||||
|
||||
For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits).
|
||||
|
44
api/v1/groupversion_info.go
Normal file
44
api/v1/groupversion_info.go
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
// Package v1 contains API Schema definitions for the v1 API group
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=onepassword.com
|
||||
package v1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
GroupVersion = schema.GroupVersion{Group: "onepassword.com", Version: "v1"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||
|
||||
// AddToScheme adds the types in this group-version to the given scheme.
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
95
api/v1/onepassworditem_types.go
Normal file
95
api/v1/onepassworditem_types.go
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||
|
||||
// OnePasswordItemSpec defines the desired state of OnePasswordItem
|
||||
type OnePasswordItemSpec struct {
|
||||
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
|
||||
ItemPath string `json:"itemPath,omitempty"`
|
||||
}
|
||||
|
||||
type OnePasswordItemConditionType string
|
||||
|
||||
const (
|
||||
// OnePasswordItemReady means the Kubernetes secret is ready for use.
|
||||
OnePasswordItemReady OnePasswordItemConditionType = "Ready"
|
||||
)
|
||||
|
||||
type OnePasswordItemCondition struct {
|
||||
// Type of job condition, Completed.
|
||||
Type OnePasswordItemConditionType `json:"type"`
|
||||
// Status of the condition, one of True, False, Unknown.
|
||||
Status metav1.ConditionStatus `json:"status"`
|
||||
// Last time the condition transit from one status to another.
|
||||
// +optional
|
||||
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
|
||||
// Human-readable message indicating details about last transition.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// OnePasswordItemStatus defines the observed state of OnePasswordItem
|
||||
type OnePasswordItemStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
|
||||
Conditions []OnePasswordItemCondition `json:"conditions"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
//+kubebuilder:subresource:status
|
||||
|
||||
// OnePasswordItem is the Schema for the onepassworditems API
|
||||
type OnePasswordItem struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
|
||||
Type string `json:"type,omitempty"`
|
||||
Spec OnePasswordItemSpec `json:"spec,omitempty"`
|
||||
Status OnePasswordItemStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
|
||||
// OnePasswordItemList contains a list of OnePasswordItem
|
||||
type OnePasswordItemList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []OnePasswordItem `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&OnePasswordItem{}, &OnePasswordItemList{})
|
||||
}
|
@@ -1,6 +1,31 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// Code generated by operator-sdk. DO NOT EDIT.
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1
|
||||
|
||||
@@ -14,8 +39,7 @@ func (in *OnePasswordItem) DeepCopyInto(out *OnePasswordItem) {
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
out.Status = in.Status
|
||||
return
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem.
|
||||
@@ -36,6 +60,22 @@ func (in *OnePasswordItem) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OnePasswordItemCondition) DeepCopyInto(out *OnePasswordItemCondition) {
|
||||
*out = *in
|
||||
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemCondition.
|
||||
func (in *OnePasswordItemCondition) DeepCopy() *OnePasswordItemCondition {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OnePasswordItemCondition)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
|
||||
*out = *in
|
||||
@@ -48,7 +88,6 @@ func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemList.
|
||||
@@ -72,7 +111,6 @@ func (in *OnePasswordItemList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OnePasswordItemSpec) DeepCopyInto(out *OnePasswordItemSpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemSpec.
|
||||
@@ -88,7 +126,13 @@ func (in *OnePasswordItemSpec) DeepCopy() *OnePasswordItemSpec {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) {
|
||||
*out = *in
|
||||
return
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]OnePasswordItemCondition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus.
|
@@ -1,15 +0,0 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:latest
|
||||
|
||||
ENV OPERATOR=/usr/local/bin/onepassword-connect-operator \
|
||||
USER_UID=1001 \
|
||||
USER_NAME=onepassword-connect-operator
|
||||
|
||||
# install operator binary
|
||||
COPY build/_output/bin/op-kubernetes-connect-operator ${OPERATOR}
|
||||
|
||||
COPY build/bin /usr/local/bin
|
||||
RUN /usr/local/bin/user_setup
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint"]
|
||||
|
||||
USER ${USER_UID}
|
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
exec ${OPERATOR} $@
|
@@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -x
|
||||
|
||||
# ensure $HOME exists and is accessible by group 0 (we don't know what the runtime UID will be)
|
||||
echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd
|
||||
mkdir -p "${HOME}"
|
||||
chown "${USER_UID}:0" "${HOME}"
|
||||
chmod ug+rwx "${HOME}"
|
||||
|
||||
# no need for this script to remain in the image after running
|
||||
rm "$0"
|
@@ -1,302 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/controller"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/apis"
|
||||
"github.com/1Password/onepassword-operator/version"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
|
||||
"github.com/operator-framework/operator-sdk/pkg/k8sutil"
|
||||
kubemetrics "github.com/operator-framework/operator-sdk/pkg/kube-metrics"
|
||||
"github.com/operator-framework/operator-sdk/pkg/leader"
|
||||
"github.com/operator-framework/operator-sdk/pkg/log/zap"
|
||||
"github.com/operator-framework/operator-sdk/pkg/metrics"
|
||||
sdkVersion "github.com/operator-framework/operator-sdk/version"
|
||||
"github.com/spf13/pflag"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
||||
)
|
||||
|
||||
const envPollingIntervalVariable = "POLLING_INTERVAL"
|
||||
const manageConnect = "MANAGE_CONNECT"
|
||||
const restartDeploymentsEnvVariable = "AUTO_RESTART"
|
||||
const defaultPollingInterval = 600
|
||||
|
||||
// Change below variables to serve metrics on different host or port.
|
||||
var (
|
||||
metricsHost = "0.0.0.0"
|
||||
metricsPort int32 = 8383
|
||||
operatorMetricsPort int32 = 8686
|
||||
)
|
||||
var log = logf.Log.WithName("cmd")
|
||||
|
||||
func printVersion() {
|
||||
log.Info(fmt.Sprintf("Operator Version: %s", version.Version))
|
||||
log.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
|
||||
log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
|
||||
log.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Add the zap logger flag set to the CLI. The flag set must
|
||||
// be added before calling pflag.Parse().
|
||||
pflag.CommandLine.AddFlagSet(zap.FlagSet())
|
||||
|
||||
// Add flags registered by imported packages (e.g. glog and
|
||||
// controller-runtime)
|
||||
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
// Use a zap logr.Logger implementation. If none of the zap
|
||||
// flags are configured (or if the zap flag set is not being
|
||||
// used), this defaults to a production zap logger.
|
||||
//
|
||||
// The logger instantiated here can be changed to any logger
|
||||
// implementing the logr.Logger interface. This logger will
|
||||
// be propagated through the whole operator, generating
|
||||
// uniform and structured logs.
|
||||
logf.SetLogger(zap.Logger())
|
||||
|
||||
printVersion()
|
||||
|
||||
namespace := os.Getenv(k8sutil.WatchNamespaceEnvVar)
|
||||
|
||||
deploymentNamespace, err := k8sutil.GetOperatorNamespace()
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to get namespace")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get a config to talk to the apiserver
|
||||
cfg, err := config.GetConfig()
|
||||
if err != nil {
|
||||
log.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// Become the leader before proceeding
|
||||
err = leader.Become(ctx, "onepassword-connect-operator-lock")
|
||||
if err != nil {
|
||||
log.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Set default manager options
|
||||
options := manager.Options{
|
||||
Namespace: namespace,
|
||||
MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort),
|
||||
}
|
||||
|
||||
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
|
||||
// Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate
|
||||
// Also note that you may face performance issues when using this with a high number of namespaces.
|
||||
if strings.Contains(namespace, ",") {
|
||||
options.Namespace = ""
|
||||
options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(namespace, ","))
|
||||
}
|
||||
|
||||
// Create a new manager to provide shared dependencies and start components
|
||||
mgr, err := manager.New(cfg, options)
|
||||
if err != nil {
|
||||
log.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Info("Registering Components.")
|
||||
|
||||
// Setup Scheme for all resources
|
||||
if err := apis.AddToScheme(mgr.GetScheme()); err != nil {
|
||||
log.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
//Setup 1PasswordConnect
|
||||
if shouldManageConnect() {
|
||||
log.Info("Automated Connect Management Enabled")
|
||||
go func() {
|
||||
connectStarted := false
|
||||
for connectStarted == false {
|
||||
err := op.SetupConnect(mgr.GetClient(), deploymentNamespace)
|
||||
// Cache Not Started is an acceptable error. Retry until cache is started.
|
||||
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
|
||||
log.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err == nil {
|
||||
connectStarted = true
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
log.Info("Automated Connect Management Disabled")
|
||||
}
|
||||
|
||||
// Setup One Password Client
|
||||
opConnectClient, err := connect.NewClientFromEnvironment()
|
||||
|
||||
if err := controller.AddToManager(mgr, opConnectClient); err != nil {
|
||||
log.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Add the Metrics Service
|
||||
addMetrics(ctx, cfg)
|
||||
|
||||
// Setup update secrets task
|
||||
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments())
|
||||
done := make(chan bool)
|
||||
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
updatedSecretsPoller.UpdateKubernetesSecretsTask()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the Cmd
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
log.Error(err, "Manager exited non-zero")
|
||||
done <- true
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// addMetrics will create the Services and Service Monitors to allow the operator export the metrics by using
|
||||
// the Prometheus operator
|
||||
func addMetrics(ctx context.Context, cfg *rest.Config) {
|
||||
// Get the namespace the operator is currently deployed in.
|
||||
operatorNs, err := k8sutil.GetOperatorNamespace()
|
||||
if err != nil {
|
||||
if errors.Is(err, k8sutil.ErrRunLocal) {
|
||||
log.Info("Skipping CR metrics server creation; not running in a cluster.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := serveCRMetrics(cfg, operatorNs); err != nil {
|
||||
log.Info("Could not generate and serve custom resource metrics", "error", err.Error())
|
||||
}
|
||||
|
||||
// Add to the below struct any other metrics ports you want to expose.
|
||||
servicePorts := []v1.ServicePort{
|
||||
{Port: metricsPort, Name: metrics.OperatorPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: metricsPort}},
|
||||
{Port: operatorMetricsPort, Name: metrics.CRPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: operatorMetricsPort}},
|
||||
}
|
||||
|
||||
// Create Service object to expose the metrics port(s).
|
||||
service, err := metrics.CreateMetricsService(ctx, cfg, servicePorts)
|
||||
if err != nil {
|
||||
log.Info("Could not create metrics Service", "error", err.Error())
|
||||
}
|
||||
|
||||
// CreateServiceMonitors will automatically create the prometheus-operator ServiceMonitor resources
|
||||
// necessary to configure Prometheus to scrape metrics from this operator.
|
||||
services := []*v1.Service{service}
|
||||
|
||||
// The ServiceMonitor is created in the same namespace where the operator is deployed
|
||||
_, err = metrics.CreateServiceMonitors(cfg, operatorNs, services)
|
||||
if err != nil {
|
||||
log.Info("Could not create ServiceMonitor object", "error", err.Error())
|
||||
// If this operator is deployed to a cluster without the prometheus-operator running, it will return
|
||||
// ErrServiceMonitorNotPresent, which can be used to safely skip ServiceMonitor creation.
|
||||
if err == metrics.ErrServiceMonitorNotPresent {
|
||||
log.Info("Install prometheus-operator in your cluster to create ServiceMonitor objects", "error", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serveCRMetrics gets the Operator/CustomResource GVKs and generates metrics based on those types.
|
||||
// It serves those metrics on "http://metricsHost:operatorMetricsPort".
|
||||
func serveCRMetrics(cfg *rest.Config, operatorNs string) error {
|
||||
// The function below returns a list of filtered operator/CR specific GVKs. For more control, override the GVK list below
|
||||
// with your own custom logic. Note that if you are adding third party API schemas, probably you will need to
|
||||
// customize this implementation to avoid permissions issues.
|
||||
filteredGVK, err := k8sutil.GetGVKsFromAddToScheme(apis.AddToScheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The metrics will be generated from the namespaces which are returned here.
|
||||
// NOTE that passing nil or an empty list of namespaces in GenerateAndServeCRMetrics will result in an error.
|
||||
ns, err := kubemetrics.GetNamespacesForMetrics(operatorNs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate and serve custom resource specific metrics.
|
||||
err = kubemetrics.GenerateAndServeCRMetrics(cfg, ns, filteredGVK, metricsHost, operatorMetricsPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPollingIntervalForUpdatingSecrets() time.Duration {
|
||||
timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable)
|
||||
if found {
|
||||
timeInSeconds, err := strconv.Atoi(timeInSecondsString)
|
||||
if err == nil {
|
||||
return time.Duration(timeInSeconds) * time.Second
|
||||
}
|
||||
log.Info("Invalid value set for polling interval. Must be a valid integer.")
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval))
|
||||
return time.Duration(defaultPollingInterval) * time.Second
|
||||
}
|
||||
|
||||
func shouldManageConnect() bool {
|
||||
shouldManageConnect, found := os.LookupEnv(manageConnect)
|
||||
if found {
|
||||
shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect))
|
||||
if err != nil {
|
||||
log.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldManageConnectBool
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldAutoRestartDeployments() bool {
|
||||
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
|
||||
if found {
|
||||
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
|
||||
if err != nil {
|
||||
log.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldAutoRestartDeploymentsBool
|
||||
}
|
||||
return false
|
||||
}
|
@@ -12,6 +12,8 @@ spec:
|
||||
app: onepassword-connect
|
||||
version: "1.0.0"
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
volumes:
|
||||
- name: shared-data
|
||||
emptyDir: {}
|
||||
@@ -32,6 +34,8 @@ spec:
|
||||
containers:
|
||||
- name: connect-api
|
||||
image: 1password/connect-api:latest
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
@@ -49,6 +53,8 @@ spec:
|
||||
name: shared-data
|
||||
- name: connect-sync
|
||||
image: 1password/connect-sync:latest
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
@@ -9,7 +9,7 @@ spec:
|
||||
ports:
|
||||
- port: 8080
|
||||
name: connect-api
|
||||
nodePort: 31080
|
||||
nodePort: 30080
|
||||
- port: 8081
|
||||
name: connect-sync
|
||||
nodePort: 31081
|
||||
nodePort: 30081
|
@@ -1,6 +1,10 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.9.2
|
||||
creationTimestamp: null
|
||||
name: onepassworditems.onepassword.com
|
||||
spec:
|
||||
group: onepassword.com
|
||||
@@ -12,8 +16,6 @@ spec:
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: OnePasswordItem is the Schema for the onepassworditems API
|
||||
@@ -38,5 +40,38 @@ spec:
|
||||
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: {}
|
21
config/crd/kustomization.yaml
Normal file
21
config/crd/kustomization.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# This kustomization.yaml is not intended to be run by itself,
|
||||
# since it depends on service name and namespace that are out of this kustomize package.
|
||||
# It should be run by config/default
|
||||
resources:
|
||||
- bases/onepassword.com_onepassworditems.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizeresource
|
||||
|
||||
patchesStrategicMerge:
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
|
||||
# patches here are for enabling the conversion webhook for each CRD
|
||||
#- patches/webhook_in_onepassworditems.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizewebhookpatch
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
|
||||
# patches here are for enabling the CA injection for each CRD
|
||||
#- patches/cainjection_in_onepassworditems.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
|
||||
|
||||
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||
configurations:
|
||||
- kustomizeconfig.yaml
|
19
config/crd/kustomizeconfig.yaml
Normal file
19
config/crd/kustomizeconfig.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
|
||||
nameReference:
|
||||
- kind: Service
|
||||
version: v1
|
||||
fieldSpecs:
|
||||
- kind: CustomResourceDefinition
|
||||
version: v1
|
||||
group: apiextensions.k8s.io
|
||||
path: spec/conversion/webhook/clientConfig/service/name
|
||||
|
||||
namespace:
|
||||
- kind: CustomResourceDefinition
|
||||
version: v1
|
||||
group: apiextensions.k8s.io
|
||||
path: spec/conversion/webhook/clientConfig/service/namespace
|
||||
create: false
|
||||
|
||||
varReference:
|
||||
- path: metadata/annotations
|
7
config/crd/patches/cainjection_in_onepassworditems.yaml
Normal file
7
config/crd/patches/cainjection_in_onepassworditems.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
# The following patch adds a directive for certmanager to inject CA into the CRD
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
|
||||
name: onepassworditems.onepassword.com
|
16
config/crd/patches/webhook_in_onepassworditems.yaml
Normal file
16
config/crd/patches/webhook_in_onepassworditems.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# The following patch enables a conversion webhook for the CRD
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: onepassworditems.onepassword.com
|
||||
spec:
|
||||
conversion:
|
||||
strategy: Webhook
|
||||
webhook:
|
||||
clientConfig:
|
||||
service:
|
||||
namespace: system
|
||||
name: webhook-service
|
||||
path: /convert
|
||||
conversionReviewVersions:
|
||||
- v1
|
143
config/default/kustomization.yaml
Normal file
143
config/default/kustomization.yaml
Normal file
@@ -0,0 +1,143 @@
|
||||
# Value of this field is prepended to the
|
||||
# names of all resources, e.g. a deployment named
|
||||
# "wordpress" becomes "alices-wordpress".
|
||||
# Note that it should also match with the prefix (text before '-') of the namespace
|
||||
# field above.
|
||||
# namePrefix: onepassword-connect-
|
||||
|
||||
# Labels to add to all resources and selectors.
|
||||
#labels:
|
||||
#- includeSelectors: true
|
||||
# pairs:
|
||||
# someName: someValue
|
||||
|
||||
resources:
|
||||
- ../crd
|
||||
- ../rbac
|
||||
- ../manager
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- ../webhook
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
|
||||
#- ../certmanager
|
||||
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||
#- ../prometheus
|
||||
|
||||
patchesStrategicMerge:
|
||||
# Protect the /metrics endpoint by putting it behind auth.
|
||||
# If you want your controller-manager to expose the /metrics
|
||||
# endpoint w/o any authn/z, please comment the following line.
|
||||
- manager_auth_proxy_patch.yaml
|
||||
|
||||
# Mount the controller config file for loading manager configurations
|
||||
# through a ComponentConfig type
|
||||
#- manager_config_patch.yaml
|
||||
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
|
||||
# crd/kustomization.yaml
|
||||
#- manager_webhook_patch.yaml
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
|
||||
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
|
||||
# 'CERTMANAGER' needs to be enabled to use ca injection
|
||||
#- webhookcainjection_patch.yaml
|
||||
|
||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
||||
# Uncomment the following replacements to add the cert-manager CA injection annotations
|
||||
#replacements:
|
||||
# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # this name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.namespace # namespace of the certificate CR
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - select:
|
||||
# kind: CustomResourceDefinition
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# name: serving-cert # this name should match the one in certificate.yaml
|
||||
# fieldPath: .metadata.name
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: ValidatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
# - select:
|
||||
# kind: MutatingWebhookConfiguration
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
# - select:
|
||||
# kind: CustomResourceDefinition
|
||||
# fieldPaths:
|
||||
# - .metadata.annotations.[cert-manager.io/inject-ca-from]
|
||||
# options:
|
||||
# delimiter: '/'
|
||||
# index: 1
|
||||
# create: true
|
||||
# - source: # Add cert-manager annotation to the webhook Service
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldPath: .metadata.name # namespace of the service
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 0
|
||||
# create: true
|
||||
# - source:
|
||||
# kind: Service
|
||||
# version: v1
|
||||
# name: webhook-service
|
||||
# fieldPath: .metadata.namespace # namespace of the service
|
||||
# targets:
|
||||
# - select:
|
||||
# kind: Certificate
|
||||
# group: cert-manager.io
|
||||
# version: v1
|
||||
# fieldPaths:
|
||||
# - .spec.dnsNames.0
|
||||
# - .spec.dnsNames.1
|
||||
# options:
|
||||
# delimiter: '.'
|
||||
# index: 1
|
||||
# create: true
|
41
config/default/manager_auth_proxy_patch.yaml
Normal file
41
config/default/manager_auth_proxy_patch.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
# This patch inject a sidecar container which is a HTTP proxy for the
|
||||
# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
containers:
|
||||
- name: kube-rbac-proxy
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1
|
||||
args:
|
||||
- "--secure-listen-address=0.0.0.0:8443"
|
||||
- "--upstream=http://127.0.0.1:8080/"
|
||||
- "--logtostderr=true"
|
||||
- "--v=0"
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
protocol: TCP
|
||||
name: https
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 5m
|
||||
memory: 64Mi
|
||||
- name: manager
|
||||
args:
|
||||
- "--health-probe-bind-address=:8081"
|
||||
- "--metrics-bind-address=127.0.0.1:8080"
|
||||
- "--leader-elect"
|
22
config/default/manager_config_patch.yaml
Normal file
22
config/default/manager_config_patch.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
containers:
|
||||
- name: manager
|
||||
args:
|
||||
- "--config=controller_manager_config.yaml"
|
||||
volumeMounts:
|
||||
- name: manager-config
|
||||
mountPath: /controller_manager_config.yaml
|
||||
subPath: controller_manager_config.yaml
|
||||
volumes:
|
||||
- name: manager-config
|
||||
configMap:
|
||||
name: manager-config
|
21
config/manager/controller_manager_config.yaml
Normal file
21
config/manager/controller_manager_config.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: controller-runtime.sigs.k8s.io/v1alpha1
|
||||
kind: ControllerManagerConfig
|
||||
health:
|
||||
healthProbeBindAddress: :8081
|
||||
metrics:
|
||||
bindAddress: 127.0.0.1:8080
|
||||
webhook:
|
||||
port: 9443
|
||||
leaderElection:
|
||||
leaderElect: true
|
||||
resourceName: c26807fd.onepassword.com
|
||||
# leaderElectionReleaseOnCancel defines if the leader should step down volume
|
||||
# when the Manager ends. This requires the binary to immediately end when the
|
||||
# Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
# speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
# LeaseDuration time first.
|
||||
# In the default scaffold provided, the program ends immediately after
|
||||
# the manager stops, so would be fine to enable this option. However,
|
||||
# if you are doing or is intended to do any operation such as perform cleanups
|
||||
# after the manager stops then its usage might be unsafe.
|
||||
# leaderElectionReleaseOnCancel: true
|
10
config/manager/kustomization.yaml
Normal file
10
config/manager/kustomization.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
resources:
|
||||
- manager.yaml
|
||||
|
||||
generatorOptions:
|
||||
disableNameSuffixHash: true
|
||||
|
||||
configMapGenerator:
|
||||
- name: manager-config
|
||||
files:
|
||||
- controller_manager_config.yaml
|
83
config/manager/manager.yaml
Normal file
83
config/manager/manager.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: onepassword-connect-operator
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/default-container: manager
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
spec:
|
||||
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: 1password/onepassword-operator:latest
|
||||
name: 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"
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8081
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
# TODO(user): Configure the resources accordingly based on the project requirements.
|
||||
# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
serviceAccountName: onepassword-connect-operator
|
||||
terminationGracePeriodSeconds: 10
|
28
config/manifests/kustomization.yaml
Normal file
28
config/manifests/kustomization.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# These resources constitute the fully configured set of manifests
|
||||
# used to generate the 'manifests/' directory in a bundle.
|
||||
resources:
|
||||
- bases/onepassword-operator.clusterserviceversion.yaml
|
||||
- ../default
|
||||
- ../samples
|
||||
- ../scorecard
|
||||
|
||||
# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix.
|
||||
# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager.
|
||||
# These patches remove the unnecessary "cert" volume and its manager container volumeMount.
|
||||
#patchesJson6902:
|
||||
#- target:
|
||||
# group: apps
|
||||
# version: v1
|
||||
# kind: Deployment
|
||||
# name: controller-manager
|
||||
# namespace: system
|
||||
# patch: |-
|
||||
# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs.
|
||||
# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment.
|
||||
# - op: remove
|
||||
|
||||
# path: /spec/template/spec/containers/0/volumeMounts/0
|
||||
# # Remove the "cert" volume, since OLM will create and mount a set of certs.
|
||||
# # Update the indices in this path if adding or removing volumes in the manager's Deployment.
|
||||
# - op: remove
|
||||
# path: /spec/template/spec/volumes/0
|
2
config/prometheus/kustomization.yaml
Normal file
2
config/prometheus/kustomization.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- monitor.yaml
|
20
config/prometheus/monitor.yaml
Normal file
20
config/prometheus/monitor.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
# Prometheus Monitor Service (Metrics)
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
name: onepassword-connect-operator-metrics-monitor
|
||||
namespace: system
|
||||
spec:
|
||||
endpoints:
|
||||
- path: /metrics
|
||||
port: https
|
||||
scheme: https
|
||||
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
tlsConfig:
|
||||
insecureSkipVerify: true
|
||||
selector:
|
||||
matchLabels:
|
||||
name: onepassword-connect-operator
|
9
config/rbac/auth_proxy_client_clusterrole.yaml
Normal file
9
config/rbac/auth_proxy_client_clusterrole.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: metrics-reader
|
||||
rules:
|
||||
- nonResourceURLs:
|
||||
- "/metrics"
|
||||
verbs:
|
||||
- get
|
17
config/rbac/auth_proxy_role.yaml
Normal file
17
config/rbac/auth_proxy_role.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: proxy-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
12
config/rbac/auth_proxy_role_binding.yaml
Normal file
12
config/rbac/auth_proxy_role_binding.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: proxy-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: proxy-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
15
config/rbac/auth_proxy_service.yaml
Normal file
15
config/rbac/auth_proxy_service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
name: onepassword-connect-operator
|
||||
name: onepassword-connect-operator-metrics-service
|
||||
namespace: system
|
||||
spec:
|
||||
ports:
|
||||
- name: https
|
||||
port: 8443
|
||||
protocol: TCP
|
||||
targetPort: https
|
||||
selector:
|
||||
name: onepassword-connect-operator
|
18
config/rbac/kustomization.yaml
Normal file
18
config/rbac/kustomization.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
resources:
|
||||
# All RBAC will be applied under this service account in
|
||||
# the deployment namespace. You may comment out this resource
|
||||
# if your manager will use a service account that exists at
|
||||
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
|
||||
# subjects if changing service account names.
|
||||
- service_account.yaml
|
||||
- role.yaml
|
||||
- role_binding.yaml
|
||||
- leader_election_role.yaml
|
||||
- leader_election_role_binding.yaml
|
||||
# Comment the following 4 lines if you want to disable
|
||||
# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
|
||||
# which protects your /metrics endpoint.
|
||||
- auth_proxy_service.yaml
|
||||
- auth_proxy_role.yaml
|
||||
- auth_proxy_role_binding.yaml
|
||||
- auth_proxy_client_clusterrole.yaml
|
37
config/rbac/leader_election_role.yaml
Normal file
37
config/rbac/leader_election_role.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
# permissions to do leader election.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: leader-election-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- update
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
12
config/rbac/leader_election_role_binding.yaml
Normal file
12
config/rbac/leader_election_role_binding.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: leader-election-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: leader-election-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
24
config/rbac/onepassworditem_editor_role.yaml
Normal file
24
config/rbac/onepassworditem_editor_role.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
# permissions for end users to edit onepassworditems.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: onepassworditem-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems/status
|
||||
verbs:
|
||||
- get
|
20
config/rbac/onepassworditem_viewer_role.yaml
Normal file
20
config/rbac/onepassworditem_viewer_role.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# permissions for end users to view onepassworditems.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: onepassworditem-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
- onepassworditems/status
|
||||
verbs:
|
||||
- get
|
@@ -1,40 +1,22 @@
|
||||
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 +25,17 @@ rules:
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
- daemonsets
|
||||
- deployments
|
||||
- replicasets
|
||||
- statefulsets
|
||||
verbs:
|
||||
@@ -59,12 +47,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 +79,21 @@ rules:
|
||||
- deployments/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- replicasets
|
||||
- deployments
|
||||
- deployments/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- monitoring.coreos.com
|
||||
resources:
|
||||
- servicemonitors
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- apiGroups:
|
||||
- onepassword.com
|
||||
resources:
|
||||
@@ -97,4 +105,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
|
12
config/rbac/role_binding.yaml
Normal file
12
config/rbac/role_binding.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: manager-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: manager-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
5
config/rbac/service_account.yaml
Normal file
5
config/rbac/service_account.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: onepassword-connect-operator
|
||||
namespace: system
|
4
config/samples/kustomization.yaml
Normal file
4
config/samples/kustomization.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
## Append samples you want in your CSV to this file as resources ##
|
||||
resources:
|
||||
- onepassword_v1_onepassworditem.yaml
|
||||
#+kubebuilder:scaffold:manifestskustomizesamples
|
@@ -1,6 +1,6 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: example
|
||||
name: onepassworditem-sample
|
||||
spec:
|
||||
itemPath: "vaults/<vault_id>/items/<item_id>"
|
7
config/scorecard/bases/config.yaml
Normal file
7
config/scorecard/bases/config.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: scorecard.operatorframework.io/v1alpha3
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: config
|
||||
stages:
|
||||
- parallel: true
|
||||
tests: []
|
16
config/scorecard/kustomization.yaml
Normal file
16
config/scorecard/kustomization.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
resources:
|
||||
- bases/config.yaml
|
||||
patchesJson6902:
|
||||
- path: patches/basic.config.yaml
|
||||
target:
|
||||
group: scorecard.operatorframework.io
|
||||
version: v1alpha3
|
||||
kind: Configuration
|
||||
name: config
|
||||
- path: patches/olm.config.yaml
|
||||
target:
|
||||
group: scorecard.operatorframework.io
|
||||
version: v1alpha3
|
||||
kind: Configuration
|
||||
name: config
|
||||
#+kubebuilder:scaffold:patchesJson6902
|
10
config/scorecard/patches/basic.config.yaml
Normal file
10
config/scorecard/patches/basic.config.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- basic-check-spec
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
labels:
|
||||
suite: basic
|
||||
test: basic-check-spec-test
|
50
config/scorecard/patches/olm.config.yaml
Normal file
50
config/scorecard/patches/olm.config.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-bundle-validation
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-bundle-validation-test
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-crds-have-validation
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-crds-have-validation-test
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-crds-have-resources
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-crds-have-resources-test
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-spec-descriptors
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-spec-descriptors-test
|
||||
- op: add
|
||||
path: /stages/0/tests/-
|
||||
value:
|
||||
entrypoint:
|
||||
- scorecard-test
|
||||
- olm-status-descriptors
|
||||
image: quay.io/operator-framework/scorecard-test:v1.23.0
|
||||
labels:
|
||||
suite: olm
|
||||
test: olm-status-descriptors-test
|
215
controllers/deployment_controller.go
Normal file
215
controllers/deployment_controller.go
Normal file
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"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"
|
||||
)
|
||||
|
||||
var logDeployment = logf.Log.WithName("controller_deployment")
|
||||
|
||||
// DeploymentReconciler reconciles a Deployment object
|
||||
type DeploymentReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
OpConnectClient connect.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.Info("Reconciling Deployment")
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
err := r.Get(context.Background(), 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.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(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, req); err != nil {
|
||||
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(secretName, deployment); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Remove the finalizer from the deployment so deletion of deployment can be completed
|
||||
if err = r.removeOnePasswordFinalizerFromDeployment(deployment); err != nil {
|
||||
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(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.Delete(context.Background(), kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DeploymentReconciler) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) {
|
||||
deployments := &appsv1.DeploymentList{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(deletedDeployment.Namespace),
|
||||
}
|
||||
|
||||
err := r.List(context.Background(), deployments, opts...)
|
||||
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(deployment *appsv1.Deployment) error {
|
||||
deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
return r.Update(context.Background(), deployment)
|
||||
}
|
||||
|
||||
func (r *DeploymentReconciler) handleApplyingDeployment(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(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.Client, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef)
|
||||
}
|
418
controllers/deployment_controller_test.go
Normal file
418
controllers/deployment_controller_test.go
Normal file
@@ -0,0 +1,418 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "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"
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentKind = "Deployment"
|
||||
deploymentAPIVersion = "v1"
|
||||
deploymentName = "test-deployment"
|
||||
)
|
||||
|
||||
var _ = Describe("Deployment controller", func() {
|
||||
var ctx context.Context
|
||||
var deploymentKey types.NamespacedName
|
||||
var secretKey types.NamespacedName
|
||||
var deploymentResource *appsv1.Deployment
|
||||
createdSecret := &v1.Secret{}
|
||||
|
||||
makeDeployment := func() {
|
||||
ctx = context.Background()
|
||||
|
||||
deploymentKey = types.NamespacedName{
|
||||
Name: deploymentName,
|
||||
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(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())
|
||||
|
||||
err = k8sClient.DeleteAllOf(context.Background(), &appsv1.Deployment{}, client.InNamespace(namespace))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
mockGetItemFunc := func() {
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
for k, v := range item1.Data {
|
||||
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
|
||||
}
|
||||
item.Version = item1.Version
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
for k, v := range item2.Data {
|
||||
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
|
||||
}
|
||||
item.Version = item2.Version
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
}
|
||||
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())
|
||||
})
|
||||
})
|
||||
})
|
214
controllers/onepassworditem_controller.go
Normal file
214
controllers/onepassworditem_controller.go
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"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
|
||||
OpConnectClient connect.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
|
||||
|
||||
// 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.Info("Reconciling OnePasswordItem")
|
||||
|
||||
onepassworditem := &onepasswordv1.OnePasswordItem{}
|
||||
err := r.Get(context.Background(), 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(context.Background(), onepassworditem); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
err = r.handleOnePasswordItem(onepassworditem, req)
|
||||
if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil {
|
||||
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(onepassworditem); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Remove finalizer now that cleanup is complete
|
||||
if err = r.removeFinalizer(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(onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer)
|
||||
if err := r.Update(context.Background(), onePasswordItem); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
kubernetesSecret := &corev1.Secret{}
|
||||
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name
|
||||
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace
|
||||
|
||||
if err := r.Delete(context.Background(), kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(opSecret *onepasswordv1.OnePasswordItem) error {
|
||||
opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer)
|
||||
return r.Update(context.Background(), opSecret)
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) handleOnePasswordItem(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(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, ownerRef)
|
||||
}
|
||||
|
||||
func (r *OnePasswordItemReconciler) updateStatus(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(context.Background(), 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,
|
||||
}
|
||||
}
|
426
controllers/onepassworditem_controller_test.go
Normal file
426
controllers/onepassworditem_controller_test.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
|
||||
. "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"
|
||||
)
|
||||
|
||||
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())
|
||||
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
for k, v := range item1.Data {
|
||||
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
|
||||
}
|
||||
item.Version = item1.Version
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
}
|
||||
})
|
||||
|
||||
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"),
|
||||
}
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
for k, v := range newData {
|
||||
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
|
||||
}
|
||||
item.Version = item1.Version + 1
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
}
|
||||
_, 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),
|
||||
}
|
||||
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
item := onepassword.Item{}
|
||||
item.Title = "!my sECReT it3m%"
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
for k, v := range testData {
|
||||
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
|
||||
}
|
||||
item.Version = item1.Version + 1
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
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())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
189
controllers/suite_test.go
Normal file
189
controllers/suite_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"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"
|
||||
//+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
|
||||
|
||||
item1 = &TestItem{
|
||||
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{
|
||||
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 {
|
||||
Name string
|
||||
Version int
|
||||
Path string
|
||||
Data map[string]string
|
||||
SecretData map[string][]byte
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
opConnectClient := &mocks.TestClient{}
|
||||
|
||||
onePasswordItemReconciler = &OnePasswordItemReconciler{
|
||||
Client: k8sManager.GetClient(),
|
||||
Scheme: k8sManager.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
}
|
||||
err = (onePasswordItemReconciler).SetupWithManager(k8sManager)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
r, _ := regexp.Compile(annotationRegExpString)
|
||||
deploymentReconciler = &DeploymentReconciler{
|
||||
Client: k8sManager.GetClient(),
|
||||
Scheme: k8sManager.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
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())
|
||||
})
|
@@ -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"
|
@@ -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
|
88
go.mod
88
go.mod
@@ -1,21 +1,81 @@
|
||||
module github.com/1Password/onepassword-operator
|
||||
|
||||
go 1.13
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/1Password/connect-sdk-go v1.0.1
|
||||
github.com/operator-framework/operator-sdk v0.19.0
|
||||
github.com/prometheus/common v0.14.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.6.1
|
||||
k8s.io/api v0.18.2
|
||||
k8s.io/apimachinery v0.18.2
|
||||
k8s.io/client-go v12.0.0+incompatible
|
||||
k8s.io/kubectl v0.18.2
|
||||
sigs.k8s.io/controller-runtime v0.6.0
|
||||
github.com/1Password/connect-sdk-go v1.5.1
|
||||
github.com/onsi/ginkgo/v2 v2.9.2
|
||||
github.com/onsi/gomega v1.27.5
|
||||
github.com/stretchr/testify v1.8.2
|
||||
k8s.io/api v0.26.3
|
||||
k8s.io/apimachinery v0.26.3
|
||||
k8s.io/client-go v0.26.3
|
||||
k8s.io/kubectl v0.26.3
|
||||
sigs.k8s.io/controller-runtime v0.14.5
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM
|
||||
k8s.io/client-go => k8s.io/client-go v0.18.2 // Required by prometheus-operator
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/zapr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.26.3 // indirect
|
||||
k8s.io/component-base v0.26.3 // indirect
|
||||
k8s.io/klog/v2 v2.90.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect
|
||||
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
23
hack/boilerplate.go.txt
Normal file
23
hack/boilerplate.go.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
286
main.go
Normal file
286
main.go
Normal file
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2022 1Password
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
k8sruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
"github.com/1Password/onepassword-operator/controllers"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
"github.com/1Password/onepassword-operator/version"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = k8sruntime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
const (
|
||||
envPollingIntervalVariable = "POLLING_INTERVAL"
|
||||
manageConnect = "MANAGE_CONNECT"
|
||||
restartDeploymentsEnvVariable = "AUTO_RESTART"
|
||||
defaultPollingInterval = 600
|
||||
|
||||
annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
|
||||
)
|
||||
|
||||
// Change below variables to serve metrics on different host or port.
|
||||
var (
|
||||
metricsHost = "0.0.0.0"
|
||||
metricsPort int32 = 8383
|
||||
operatorMetricsPort int32 = 8686
|
||||
)
|
||||
|
||||
func printVersion() {
|
||||
setupLog.Info(fmt.Sprintf("Operator Version: %s", version.OperatorVersion))
|
||||
setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
|
||||
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
|
||||
setupLog.Info(fmt.Sprintf("Version of operator-sdk: %v", version.OperatorSDKVersion))
|
||||
}
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(onepasswordcomv1.AddToScheme(scheme))
|
||||
//+kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
printVersion()
|
||||
|
||||
watchNamespace, err := getWatchNamespace()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to get WatchNamespace, "+
|
||||
"the manager will watch and manage resources in all namespaces")
|
||||
}
|
||||
|
||||
deploymentNamespace, err := utils.GetOperatorNamespace()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "Failed to get namespace")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
options := ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Namespace: watchNamespace,
|
||||
MetricsBindAddress: metricsAddr,
|
||||
Port: 9443,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "c26807fd.onepassword.com",
|
||||
}
|
||||
|
||||
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
|
||||
if strings.Contains(watchNamespace, ",") {
|
||||
setupLog.Info("manager set up with multiple namespaces", "namespaces", watchNamespace)
|
||||
// configure cluster-scoped with MultiNamespacedCacheBuilder
|
||||
options.Namespace = ""
|
||||
options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(watchNamespace, ","))
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup One Password Client
|
||||
opConnectClient, err := connect.NewClientFromEnvironment()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to create Connect client")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controllers.OnePasswordItemReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
r, _ := regexp.Compile(annotationRegExpString)
|
||||
if err = (&controllers.DeploymentReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
OpConnectClient: opConnectClient,
|
||||
OpAnnotationRegExp: r,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Deployment")
|
||||
os.Exit(1)
|
||||
}
|
||||
//+kubebuilder:scaffold:builder
|
||||
|
||||
//Setup 1PasswordConnect
|
||||
if shouldManageConnect() {
|
||||
setupLog.Info("Automated Connect Management Enabled")
|
||||
go func() {
|
||||
connectStarted := false
|
||||
for connectStarted == false {
|
||||
err := op.SetupConnect(mgr.GetClient(), deploymentNamespace)
|
||||
// Cache Not Started is an acceptable error. Retry until cache is started.
|
||||
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err == nil {
|
||||
connectStarted = true
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
setupLog.Info("Automated Connect Management Disabled")
|
||||
}
|
||||
|
||||
// Setup update secrets task
|
||||
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments())
|
||||
done := make(chan bool)
|
||||
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := updatedSecretsPoller.UpdateKubernetesSecretsTask()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "error running update kubernetes secret task")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// getWatchNamespace returns the Namespace the operator should be watching for changes
|
||||
func getWatchNamespace() (string, error) {
|
||||
// WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE
|
||||
// which specifies the Namespace to watch.
|
||||
// An empty value means the operator is running with cluster scope.
|
||||
var watchNamespaceEnvVar = "WATCH_NAMESPACE"
|
||||
|
||||
ns, found := os.LookupEnv(watchNamespaceEnvVar)
|
||||
if !found {
|
||||
return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar)
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func shouldManageConnect() bool {
|
||||
shouldManageConnect, found := os.LookupEnv(manageConnect)
|
||||
if found {
|
||||
shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldManageConnectBool
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldAutoRestartDeployments() bool {
|
||||
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
|
||||
if found {
|
||||
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "")
|
||||
os.Exit(1)
|
||||
}
|
||||
return shouldAutoRestartDeploymentsBool
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getPollingIntervalForUpdatingSecrets() time.Duration {
|
||||
timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable)
|
||||
if found {
|
||||
timeInSeconds, err := strconv.Atoi(timeInSecondsString)
|
||||
if err == nil {
|
||||
return time.Duration(timeInSeconds) * time.Second
|
||||
}
|
||||
setupLog.Info("Invalid value set for polling interval. Must be a valid integer.")
|
||||
}
|
||||
|
||||
setupLog.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval))
|
||||
return time.Duration(defaultPollingInterval) * time.Second
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
v1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the types with the Scheme so the components can map objects to GroupVersionKinds and back
|
||||
AddToSchemes = append(AddToSchemes, v1.SchemeBuilder.AddToScheme)
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// AddToSchemes may be used to add all resources defined in the project to a Scheme
|
||||
var AddToSchemes runtime.SchemeBuilder
|
||||
|
||||
// AddToScheme adds all Resources to the Scheme
|
||||
func AddToScheme(s *runtime.Scheme) error {
|
||||
return AddToSchemes.AddToScheme(s)
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
// Package onepassword contains onepassword API versions.
|
||||
//
|
||||
// This file ensures Go source parsers acknowledge the onepassword package
|
||||
// and any child packages. It can be removed if any other Go source files are
|
||||
// added to this package.
|
||||
package onepassword
|
@@ -1,4 +0,0 @@
|
||||
// Package v1 contains API Schema definitions for the onepassword v1 API group
|
||||
// +k8s:deepcopy-gen=package,register
|
||||
// +groupName=onepassword.com
|
||||
package v1
|
@@ -1,45 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||
|
||||
// OnePasswordItemSpec defines the desired state of OnePasswordItem
|
||||
type OnePasswordItemSpec struct {
|
||||
ItemPath string `json:"itemPath,omitempty"`
|
||||
}
|
||||
|
||||
// OnePasswordItemStatus defines the observed state of OnePasswordItem
|
||||
type OnePasswordItemStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
|
||||
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// OnePasswordItem is the Schema for the onepassworditems API
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:path=onepassworditems,scope=Namespaced
|
||||
type OnePasswordItem struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec OnePasswordItemSpec `json:"spec,omitempty"`
|
||||
Status OnePasswordItemStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// OnePasswordItemList contains a list of OnePasswordItem
|
||||
type OnePasswordItemList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []OnePasswordItem `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&OnePasswordItem{}, &OnePasswordItemList{})
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
// NOTE: Boilerplate only. Ignore this file.
|
||||
|
||||
// Package v1 contains API Schema definitions for the onepassword v1 API group
|
||||
// +k8s:deepcopy-gen=package,register
|
||||
// +groupName=onepassword.com
|
||||
package v1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
var (
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
SchemeGroupVersion = schema.GroupVersion{Group: "onepassword.com", Version: "v1"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
|
||||
)
|
@@ -1,10 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/1Password/onepassword-operator/pkg/controller/deployment"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
|
||||
AddToManagerFuncs = append(AddToManagerFuncs, deployment.Add)
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/1Password/onepassword-operator/pkg/controller/onepassworditem"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
|
||||
AddToManagerFuncs = append(AddToManagerFuncs, onepassworditem.Add)
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
)
|
||||
|
||||
// AddToManagerFuncs is a list of functions to add all Controllers to the Manager
|
||||
var AddToManagerFuncs []func(manager.Manager, connect.Client) error
|
||||
|
||||
// AddToManager adds all Controllers to the Manager
|
||||
func AddToManager(m manager.Manager, opConnectClient connect.Client) error {
|
||||
for _, f := range AddToManagerFuncs {
|
||||
if err := f(m, opConnectClient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -1,205 +0,0 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
"regexp"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
)
|
||||
|
||||
var log = logf.Log.WithName("controller_deployment")
|
||||
var finalizer = "onepassword.com/finalizer.secret"
|
||||
|
||||
const annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
|
||||
|
||||
func Add(mgr manager.Manager, opConnectClient connect.Client) error {
|
||||
return add(mgr, newReconciler(mgr, opConnectClient))
|
||||
}
|
||||
|
||||
func newReconciler(mgr manager.Manager, opConnectClient connect.Client) *ReconcileDeployment {
|
||||
r, _ := regexp.Compile(annotationRegExpString)
|
||||
return &ReconcileDeployment{
|
||||
opAnnotationRegExp: r,
|
||||
kubeClient: mgr.GetClient(),
|
||||
scheme: mgr.GetScheme(),
|
||||
opConnectClient: opConnectClient,
|
||||
}
|
||||
}
|
||||
|
||||
func add(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
c, err := controller.New("deployment-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to primary resource Deployment
|
||||
err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ reconcile.Reconciler = &ReconcileDeployment{}
|
||||
|
||||
type ReconcileDeployment struct {
|
||||
opAnnotationRegExp *regexp.Regexp
|
||||
kubeClient client.Client
|
||||
scheme *runtime.Scheme
|
||||
opConnectClient connect.Client
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&appsv1.Deployment{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) test() {
|
||||
return
|
||||
}
|
||||
|
||||
// Reconcile reads that state of the cluster for a Deployment object and makes changes based on the state read
|
||||
// and what is in the Deployment.Spec
|
||||
// Note:
|
||||
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
|
||||
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
|
||||
func (r *ReconcileDeployment) Reconcile(request reconcile.Request) (reconcile.Result, error) {
|
||||
ctx := context.Background()
|
||||
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
reqLogger.Info("Reconciling Deployment")
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
err := r.kubeClient.Get(ctx, request.NamespacedName, deployment)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.opAnnotationRegExp)
|
||||
if !annotationsFound {
|
||||
reqLogger.Info("No 1Password Annotations found")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
//If the deployment is not being deleted
|
||||
if deployment.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// Adds a finalizer to the deployment if one does not exist.
|
||||
// This is so we can handle cleanup of associated secrets properly
|
||||
if !utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
|
||||
deployment.ObjectMeta.Finalizers = append(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
if err := r.kubeClient.Update(context.Background(), deployment); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
if err := r.HandleApplyingDeployment(deployment.Namespace, annotations, request); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
// The deployment has been marked for deletion. If the one password
|
||||
// finalizer is found there are cleanup tasks to perform
|
||||
if utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) {
|
||||
|
||||
secretName := annotations[op.NameAnnotation]
|
||||
r.cleanupKubernetesSecretForDeployment(secretName, deployment)
|
||||
|
||||
// Remove the finalizer from the deployment so deletion of deployment can be completed
|
||||
if err := r.removeOnePasswordFinalizerFromDeployment(deployment); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) cleanupKubernetesSecretForDeployment(secretName string, deletedDeployment *appsv1.Deployment) error {
|
||||
kubernetesSecret := &corev1.Secret{}
|
||||
kubernetesSecret.ObjectMeta.Name = secretName
|
||||
kubernetesSecret.ObjectMeta.Namespace = deletedDeployment.Namespace
|
||||
|
||||
if len(secretName) == 0 {
|
||||
return nil
|
||||
}
|
||||
updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret}
|
||||
|
||||
multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only delete the associated kubernetes secret if it is not being used by other deployments
|
||||
if !multipleDeploymentsUsingSecret {
|
||||
if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) {
|
||||
deployments := &appsv1.DeploymentList{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(deletedDeployment.Namespace),
|
||||
}
|
||||
|
||||
err := r.kubeClient.List(context.Background(), deployments, opts...)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to list kubernetes deployments")
|
||||
return false, err
|
||||
}
|
||||
|
||||
for i := 0; i < len(deployments.Items); i++ {
|
||||
if deployments.Items[i].Name != deletedDeployment.Name {
|
||||
if op.IsDeploymentUsingSecrets(&deployments.Items[i], updatedSecrets) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deployment *appsv1.Deployment) error {
|
||||
deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer)
|
||||
return r.kubeClient.Update(context.Background(), deployment)
|
||||
}
|
||||
|
||||
func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotations map[string]string, request reconcile.Request) error {
|
||||
reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
|
||||
secretName := annotations[op.NameAnnotation]
|
||||
if len(secretName) == 0 {
|
||||
reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.")
|
||||
return nil
|
||||
}
|
||||
|
||||
item, err := op.GetOnePasswordItemByPath(r.opConnectClient, annotations[op.ItemPathAnnotation])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve item: %v", err)
|
||||
}
|
||||
|
||||
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation])
|
||||
}
|
@@ -1,474 +0,0 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/stretchr/testify/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
errors2 "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentKind = "Deployment"
|
||||
deploymentAPIVersion = "v1"
|
||||
name = "test-deployment"
|
||||
namespace = "default"
|
||||
vaultId = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
itemId = "nwrhuano7bcwddcviubpp4mhfq"
|
||||
username = "test-user"
|
||||
password = "QmHumKc$mUeEem7caHtbaBaJ"
|
||||
userKey = "username"
|
||||
passKey = "password"
|
||||
version = 123
|
||||
)
|
||||
|
||||
type testReconcileItem struct {
|
||||
testName string
|
||||
deploymentResource *appsv1.Deployment
|
||||
existingSecret *corev1.Secret
|
||||
expectedError error
|
||||
expectedResultSecret *corev1.Secret
|
||||
expectedEvents []string
|
||||
opItem map[string]string
|
||||
existingDeployment *appsv1.Deployment
|
||||
}
|
||||
|
||||
var (
|
||||
expectedSecretData = map[string][]byte{
|
||||
"password": []byte(password),
|
||||
"username": []byte(username),
|
||||
}
|
||||
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
|
||||
)
|
||||
|
||||
var (
|
||||
time = metav1.Now()
|
||||
regex, _ = regexp.Compile(annotationRegExpString)
|
||||
)
|
||||
|
||||
var tests = []testReconcileItem{
|
||||
{
|
||||
testName: "Test Delete Deployment where secret is being used in another deployment's volumes",
|
||||
deploymentResource: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
DeletionTimestamp: &time,
|
||||
Finalizers: []string{
|
||||
finalizer,
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: itemPath,
|
||||
op.NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "another-deployment",
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: itemPath,
|
||||
op.NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: name,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Test Delete Deployment where secret is being used in another deployment's container",
|
||||
deploymentResource: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
DeletionTimestamp: &time,
|
||||
Finalizers: []string{
|
||||
finalizer,
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: itemPath,
|
||||
op.NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "another-deployment",
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: itemPath,
|
||||
op.NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: name,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Key: passKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Test Delete Deployment",
|
||||
deploymentResource: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
DeletionTimestamp: &time,
|
||||
Finalizers: []string{
|
||||
finalizer,
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: itemPath,
|
||||
op.NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: nil,
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Test Do not update if OnePassword Item Version has not changed",
|
||||
deploymentResource: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: itemPath,
|
||||
op.NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: "data we don't expect to have updated",
|
||||
passKey: "data we don't expect to have updated",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Test Updating Existing Kubernetes Secret using Deployment",
|
||||
deploymentResource: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: itemPath,
|
||||
op.NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: "456",
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Create Deployment",
|
||||
deploymentResource: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.ItemPathAnnotation: itemPath,
|
||||
op.NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: nil,
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestReconcileDepoyment(t *testing.T) {
|
||||
for _, testData := range tests {
|
||||
t.Run(testData.testName, func(t *testing.T) {
|
||||
|
||||
// Register operator types with the runtime scheme.
|
||||
s := scheme.Scheme
|
||||
s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.deploymentResource)
|
||||
|
||||
// Objects to track in the fake client.
|
||||
objs := []runtime.Object{
|
||||
testData.deploymentResource,
|
||||
}
|
||||
|
||||
if testData.existingSecret != nil {
|
||||
objs = append(objs, testData.existingSecret)
|
||||
}
|
||||
|
||||
if testData.existingDeployment != nil {
|
||||
objs = append(objs, testData.existingDeployment)
|
||||
}
|
||||
|
||||
// Create a fake client to mock API calls.
|
||||
cl := fake.NewFakeClientWithScheme(s, objs...)
|
||||
// Create a Deployment object with the scheme and mock kubernetes
|
||||
// and 1Password Connect client.
|
||||
|
||||
opConnectClient := &mocks.TestClient{}
|
||||
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
|
||||
item.Version = version
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
}
|
||||
r := &ReconcileDeployment{
|
||||
kubeClient: cl,
|
||||
scheme: s,
|
||||
opConnectClient: opConnectClient,
|
||||
opAnnotationRegExp: regex,
|
||||
}
|
||||
|
||||
// Mock request to simulate Reconcile() being called on an event for a
|
||||
// watched resource .
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
_, err := r.Reconcile(req)
|
||||
|
||||
assert.Equal(t, testData.expectedError, err)
|
||||
|
||||
var expectedSecretName string
|
||||
if testData.expectedResultSecret == nil {
|
||||
expectedSecretName = testData.deploymentResource.Name
|
||||
} else {
|
||||
expectedSecretName = testData.expectedResultSecret.Name
|
||||
}
|
||||
|
||||
// Check if Secret has been created and has the correct data
|
||||
secret := &corev1.Secret{}
|
||||
err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret)
|
||||
|
||||
if testData.expectedResultSecret == nil {
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors2.IsNotFound(err))
|
||||
} else {
|
||||
assert.Equal(t, testData.expectedResultSecret.Data, secret.Data)
|
||||
assert.Equal(t, testData.expectedResultSecret.Name, secret.Name)
|
||||
assert.Equal(t, testData.expectedResultSecret.Type, secret.Type)
|
||||
assert.Equal(t, testData.expectedResultSecret.Annotations[op.VersionAnnotation], secret.Annotations[op.VersionAnnotation])
|
||||
|
||||
updatedCR := &appsv1.Deployment{}
|
||||
err = cl.Get(context.TODO(), req.NamespacedName, updatedCR)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generateFields(username, password string) []*onepassword.ItemField {
|
||||
fields := []*onepassword.ItemField{
|
||||
{
|
||||
Label: "username",
|
||||
Value: username,
|
||||
},
|
||||
{
|
||||
Label: "password",
|
||||
Value: password,
|
||||
},
|
||||
}
|
||||
return fields
|
||||
}
|
@@ -1,154 +0,0 @@
|
||||
package onepassworditem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
)
|
||||
|
||||
var log = logf.Log.WithName("controller_onepassworditem")
|
||||
var finalizer = "onepassword.com/finalizer.secret"
|
||||
|
||||
func Add(mgr manager.Manager, opConnectClient connect.Client) error {
|
||||
return add(mgr, newReconciler(mgr, opConnectClient))
|
||||
}
|
||||
|
||||
func newReconciler(mgr manager.Manager, opConnectClient connect.Client) *ReconcileOnePasswordItem {
|
||||
return &ReconcileOnePasswordItem{
|
||||
kubeClient: mgr.GetClient(),
|
||||
scheme: mgr.GetScheme(),
|
||||
opConnectClient: opConnectClient,
|
||||
}
|
||||
}
|
||||
|
||||
func add(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
c, err := controller.New("onepassworditem-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to primary resource OnePasswordItem
|
||||
err = c.Watch(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ reconcile.Reconciler = &ReconcileOnePasswordItem{}
|
||||
|
||||
type ReconcileOnePasswordItem struct {
|
||||
kubeClient kubeClient.Client
|
||||
scheme *runtime.Scheme
|
||||
opConnectClient connect.Client
|
||||
}
|
||||
|
||||
func (r *ReconcileOnePasswordItem) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&onepasswordv1.OnePasswordItem{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *ReconcileOnePasswordItem) Reconcile(request reconcile.Request) (reconcile.Result, error) {
|
||||
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
reqLogger.Info("Reconciling OnePasswordItem")
|
||||
|
||||
onepassworditem := &onepasswordv1.OnePasswordItem{}
|
||||
err := r.kubeClient.Get(context.Background(), request.NamespacedName, onepassworditem)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
// If the deployment is not being deleted
|
||||
if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// Adds a finalizer to the deployment if one does not exist.
|
||||
// This is so we can handle cleanup of associated secrets properly
|
||||
if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||
onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer)
|
||||
if err := r.kubeClient.Update(context.Background(), onepassworditem); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handles creation or updating secrets for deployment if needed
|
||||
if err := r.HandleOnePasswordItem(onepassworditem, request); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
// If one password finalizer exists then we must cleanup associated secrets
|
||||
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||
|
||||
// Delete associated kubernetes secret
|
||||
if err = r.cleanupKubernetesSecret(onepassworditem); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
// Remove finalizer now that cleanup is complete
|
||||
if err := r.removeFinalizer(onepassworditem); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ReconcileOnePasswordItem) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer)
|
||||
if err := r.kubeClient.Update(context.Background(), onePasswordItem); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReconcileOnePasswordItem) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error {
|
||||
kubernetesSecret := &corev1.Secret{}
|
||||
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name
|
||||
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace
|
||||
|
||||
r.kubeClient.Delete(context.Background(), kubernetesSecret)
|
||||
if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem(opSecret *onepasswordv1.OnePasswordItem) error {
|
||||
opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer)
|
||||
return r.kubeClient.Update(context.Background(), opSecret)
|
||||
}
|
||||
|
||||
func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
|
||||
secretName := resource.GetName()
|
||||
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
|
||||
|
||||
item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve item: %v", err)
|
||||
}
|
||||
|
||||
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart)
|
||||
}
|
@@ -1,387 +0,0 @@
|
||||
package onepassworditem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
op "github.com/1Password/onepassword-operator/pkg/onepassword"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
errors2 "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
onePasswordItemKind = "OnePasswordItem"
|
||||
onePasswordItemAPIVersion = "onepassword.com/v1"
|
||||
name = "test"
|
||||
namespace = "default"
|
||||
vaultId = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
itemId = "nwrhuano7bcwddcviubpp4mhfq"
|
||||
username = "test-user"
|
||||
password = "QmHumKc$mUeEem7caHtbaBaJ"
|
||||
firstHost = "http://localhost:8080"
|
||||
awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
iceCream = "freezing blue 20%"
|
||||
userKey = "username"
|
||||
passKey = "password"
|
||||
version = 123
|
||||
)
|
||||
|
||||
type testReconcileItem struct {
|
||||
testName string
|
||||
customResource *onepasswordv1.OnePasswordItem
|
||||
existingSecret *corev1.Secret
|
||||
expectedError error
|
||||
expectedResultSecret *corev1.Secret
|
||||
expectedEvents []string
|
||||
opItem map[string]string
|
||||
existingOnePasswordItem *onepasswordv1.OnePasswordItem
|
||||
}
|
||||
|
||||
var (
|
||||
expectedSecretData = map[string][]byte{
|
||||
"password": []byte(password),
|
||||
"username": []byte(username),
|
||||
}
|
||||
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
|
||||
)
|
||||
|
||||
var (
|
||||
time = metav1.Now()
|
||||
)
|
||||
|
||||
var tests = []testReconcileItem{
|
||||
{
|
||||
testName: "Test Delete OnePasswordItem",
|
||||
customResource: &onepasswordv1.OnePasswordItem{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: onePasswordItemKind,
|
||||
APIVersion: onePasswordItemAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
DeletionTimestamp: &time,
|
||||
Finalizers: []string{
|
||||
finalizer,
|
||||
},
|
||||
},
|
||||
Spec: onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: itemPath,
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: nil,
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Test Do not update if OnePassword Version has not changed",
|
||||
customResource: &onepasswordv1.OnePasswordItem{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: onePasswordItemKind,
|
||||
APIVersion: onePasswordItemAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: itemPath,
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: "data we don't expect to have updated",
|
||||
passKey: "data we don't expect to have updated",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Test Updating Existing Kubernetes Secret using OnePasswordItem",
|
||||
customResource: &onepasswordv1.OnePasswordItem{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: onePasswordItemKind,
|
||||
APIVersion: onePasswordItemAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: itemPath,
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: "456",
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Custom secret type",
|
||||
customResource: &onepasswordv1.OnePasswordItem{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: onePasswordItemKind,
|
||||
APIVersion: onePasswordItemAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: itemPath,
|
||||
},
|
||||
},
|
||||
existingSecret: nil,
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Secret from 1Password item with invalid K8s labels",
|
||||
customResource: &onepasswordv1.OnePasswordItem{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: onePasswordItemKind,
|
||||
APIVersion: onePasswordItemAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "!my sECReT it3m%",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: itemPath,
|
||||
},
|
||||
},
|
||||
existingSecret: nil,
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-secret-it3m",
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Secret from 1Password item with fields and sections that have invalid K8s labels",
|
||||
customResource: &onepasswordv1.OnePasswordItem{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: onePasswordItemKind,
|
||||
APIVersion: onePasswordItemAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "!my sECReT it3m%",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: onepasswordv1.OnePasswordItemSpec{
|
||||
ItemPath: itemPath,
|
||||
},
|
||||
},
|
||||
existingSecret: nil,
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-secret-it3m",
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
op.VersionAnnotation: fmt.Sprint(version),
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"password": []byte(password),
|
||||
"username": []byte(username),
|
||||
"first-host": []byte(firstHost),
|
||||
"aws-access-key": []byte(awsKey),
|
||||
"ice-cream-type": []byte(iceCream),
|
||||
},
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
"first host": firstHost,
|
||||
"AWS Access Key": awsKey,
|
||||
"😄 ice-cream type": iceCream,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestReconcileOnePasswordItem(t *testing.T) {
|
||||
for _, testData := range tests {
|
||||
t.Run(testData.testName, func(t *testing.T) {
|
||||
|
||||
// Register operator types with the runtime scheme.
|
||||
s := scheme.Scheme
|
||||
s.AddKnownTypes(onepasswordv1.SchemeGroupVersion, testData.customResource)
|
||||
|
||||
// Objects to track in the fake client.
|
||||
objs := []runtime.Object{
|
||||
testData.customResource,
|
||||
}
|
||||
|
||||
if testData.existingSecret != nil {
|
||||
objs = append(objs, testData.existingSecret)
|
||||
}
|
||||
|
||||
if testData.existingOnePasswordItem != nil {
|
||||
objs = append(objs, testData.existingOnePasswordItem)
|
||||
}
|
||||
// Create a fake client to mock API calls.
|
||||
cl := fake.NewFakeClientWithScheme(s, objs...)
|
||||
// Create a OnePasswordItem object with the scheme and mock kubernetes
|
||||
// and 1Password Connect client.
|
||||
|
||||
opConnectClient := &mocks.TestClient{}
|
||||
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = []*onepassword.ItemField{}
|
||||
for k, v := range testData.opItem {
|
||||
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
|
||||
}
|
||||
item.Version = version
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
}
|
||||
r := &ReconcileOnePasswordItem{
|
||||
kubeClient: cl,
|
||||
scheme: s,
|
||||
opConnectClient: opConnectClient,
|
||||
}
|
||||
|
||||
// Mock request to simulate Reconcile() being called on an event for a
|
||||
// watched resource .
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: testData.customResource.ObjectMeta.Name,
|
||||
Namespace: testData.customResource.ObjectMeta.Namespace,
|
||||
},
|
||||
}
|
||||
_, err := r.Reconcile(req)
|
||||
|
||||
assert.Equal(t, testData.expectedError, err)
|
||||
|
||||
var expectedSecretName string
|
||||
if testData.expectedResultSecret == nil {
|
||||
expectedSecretName = testData.customResource.Name
|
||||
} else {
|
||||
expectedSecretName = testData.expectedResultSecret.Name
|
||||
}
|
||||
|
||||
// Check if Secret has been created and has the correct data
|
||||
secret := &corev1.Secret{}
|
||||
err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret)
|
||||
|
||||
if testData.expectedResultSecret == nil {
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors2.IsNotFound(err))
|
||||
} else {
|
||||
assert.Equal(t, testData.expectedResultSecret.Data, secret.Data)
|
||||
assert.Equal(t, testData.expectedResultSecret.Name, secret.Name)
|
||||
assert.Equal(t, testData.expectedResultSecret.Type, secret.Type)
|
||||
assert.Equal(t, testData.expectedResultSecret.Annotations[op.VersionAnnotation], secret.Annotations[op.VersionAnnotation])
|
||||
|
||||
updatedCR := &onepasswordv1.OnePasswordItem{}
|
||||
err = cl.Get(context.TODO(), req.NamespacedName, updatedCR)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generateFields(username, password string) []*onepassword.ItemField {
|
||||
fields := []*onepassword.ItemField{
|
||||
{
|
||||
Label: "username",
|
||||
Value: username,
|
||||
},
|
||||
{
|
||||
Label: "password",
|
||||
Value: password,
|
||||
},
|
||||
}
|
||||
return fields
|
||||
}
|
@@ -7,7 +7,12 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"reflect"
|
||||
|
||||
errs "errors"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -22,28 +27,30 @@ import (
|
||||
const OnepasswordPrefix = "operator.1password.io"
|
||||
const NameAnnotation = OnepasswordPrefix + "/item-name"
|
||||
const VersionAnnotation = OnepasswordPrefix + "/item-version"
|
||||
const restartAnnotation = OnepasswordPrefix + "/last-restarted"
|
||||
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
|
||||
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
|
||||
|
||||
var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable")
|
||||
|
||||
var log = logf.Log
|
||||
|
||||
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string) error {
|
||||
|
||||
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error {
|
||||
itemVersion := fmt.Sprint(item.Version)
|
||||
annotations := map[string]string{
|
||||
secretAnnotations := map[string]string{
|
||||
VersionAnnotation: itemVersion,
|
||||
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)
|
||||
}
|
||||
annotations[RestartDeploymentsAnnotation] = autoRestart
|
||||
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
|
||||
}
|
||||
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, annotations, *item)
|
||||
|
||||
// "Opaque" and "" secret types are treated the same by Kubernetes.
|
||||
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item, ownerRef)
|
||||
|
||||
currentSecret := &corev1.Secret{}
|
||||
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
|
||||
@@ -54,42 +61,88 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
|
||||
return err
|
||||
}
|
||||
|
||||
if currentSecret.Annotations[VersionAnnotation] != itemVersion {
|
||||
// Check if the secret types are being changed on the update.
|
||||
// Avoid Opaque and "" are treated as different on check.
|
||||
wantSecretType := secretType
|
||||
if wantSecretType == "" {
|
||||
wantSecretType = string(corev1.SecretTypeOpaque)
|
||||
}
|
||||
currentSecretType := string(currentSecret.Type)
|
||||
if currentSecretType == "" {
|
||||
currentSecretType = string(corev1.SecretTypeOpaque)
|
||||
}
|
||||
if currentSecretType != wantSecretType {
|
||||
return ErrCannotUpdateSecretType
|
||||
}
|
||||
|
||||
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 = annotations
|
||||
currentSecret.ObjectMeta.Annotations = secretAnnotations
|
||||
currentSecret.ObjectMeta.Labels = labels
|
||||
currentSecret.Data = secret.Data
|
||||
return kubeClient.Update(context.Background(), currentSecret)
|
||||
if err := kubeClient.Update(context.Background(), 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, item onepassword.Item) *corev1.Secret {
|
||||
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret {
|
||||
var ownerRefs []metav1.OwnerReference
|
||||
if ownerRef != nil {
|
||||
ownerRefs = []metav1.OwnerReference{*ownerRef}
|
||||
}
|
||||
|
||||
return &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: formatSecretName(name),
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
Name: formatSecretName(name),
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
Labels: labels,
|
||||
OwnerReferences: ownerRefs,
|
||||
},
|
||||
Data: BuildKubernetesSecretData(item.Fields),
|
||||
Data: BuildKubernetesSecretData(item.Fields, item.Files),
|
||||
Type: corev1.SecretType(secretType),
|
||||
}
|
||||
}
|
||||
|
||||
func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byte {
|
||||
func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte {
|
||||
secretData := map[string][]byte{}
|
||||
for i := 0; i < len(fields); i++ {
|
||||
if fields[i].Value != "" {
|
||||
key := formatSecretName(fields[i].Label)
|
||||
key := formatSecretDataName(fields[i].Label)
|
||||
secretData[key] = []byte(fields[i].Value)
|
||||
}
|
||||
}
|
||||
|
||||
// populate unpopulated fields from files
|
||||
for _, file := range files {
|
||||
content, err := file.Content()
|
||||
if err != nil {
|
||||
log.Error(err, "Could not load contents of file %s", file.Name)
|
||||
continue
|
||||
}
|
||||
if content != nil {
|
||||
key := file.Name
|
||||
if secretData[key] == nil {
|
||||
secretData[key] = content
|
||||
} else {
|
||||
log.Info(fmt.Sprintf("File '%s' ignored because of a field with the same name", file.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
return secretData
|
||||
}
|
||||
|
||||
// formatSecretName rewrites a value to be a valid Secret name or Secret data key.
|
||||
// formatSecretName rewrites a value to be a valid Secret name.
|
||||
//
|
||||
// The Secret meta.name and data keys must be valid DNS subdomain names (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
|
||||
// The Secret meta.name and data keys must be valid DNS subdomain names
|
||||
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
|
||||
func formatSecretName(value string) string {
|
||||
if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 {
|
||||
return value
|
||||
@@ -97,7 +150,18 @@ func formatSecretName(value string) string {
|
||||
return createValidSecretName(value)
|
||||
}
|
||||
|
||||
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-]+")
|
||||
// formatSecretDataName rewrites a value to be a valid Secret data key.
|
||||
//
|
||||
// The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.`
|
||||
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
|
||||
func formatSecretDataName(value string) string {
|
||||
if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 {
|
||||
return value
|
||||
}
|
||||
return createValidSecretDataName(value)
|
||||
}
|
||||
|
||||
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+")
|
||||
|
||||
func createValidSecretName(value string) string {
|
||||
result := strings.ToLower(value)
|
||||
@@ -108,5 +172,19 @@ func createValidSecretName(value string) string {
|
||||
}
|
||||
|
||||
// first and last character MUST be alphanumeric
|
||||
return strings.Trim(result, "-")
|
||||
return strings.Trim(result, "-.")
|
||||
}
|
||||
|
||||
var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+")
|
||||
var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)")
|
||||
|
||||
func createValidSecretDataName(value string) string {
|
||||
result := invalidStartEndChars.ReplaceAllString(value, "")
|
||||
result = invalidDataChars.ReplaceAllString(result, "-")
|
||||
|
||||
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
|
||||
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
@@ -3,23 +3,20 @@ package kubernetessecrets
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
||||
"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"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
const restartDeploymentAnnotation = "false"
|
||||
|
||||
type k8s struct {
|
||||
clientset kubernetes.Interface
|
||||
}
|
||||
|
||||
func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
secretName := "test-secret-name"
|
||||
namespace := "test"
|
||||
@@ -30,8 +27,11 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation)
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
@@ -45,6 +45,51 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
compareAnnotationsToItem(createdSecret.Annotations, item, t)
|
||||
}
|
||||
|
||||
func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
|
||||
secretName := "test-secret-name"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
Name: "test-deployment",
|
||||
UID: types.UID("test-uid"),
|
||||
}
|
||||
err := CreateKubernetesSecretFromItem(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)
|
||||
|
||||
// Check owner references.
|
||||
gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences
|
||||
if len(gotOwnerRefs) != 1 {
|
||||
t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs))
|
||||
}
|
||||
|
||||
expOwnerRef := metav1.OwnerReference{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
Name: "test-deployment",
|
||||
UID: types.UID("test-uid"),
|
||||
}
|
||||
gotOwnerRef := gotOwnerRefs[0]
|
||||
if gotOwnerRef != expOwnerRef {
|
||||
t.Errorf("Expected owner reference value: %v but got: %v", expOwnerRef, gotOwnerRef)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
secretName := "test-secret-update"
|
||||
namespace := "test"
|
||||
@@ -55,8 +100,12 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation)
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
@@ -67,7 +116,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
newItem.Version = 456
|
||||
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation)
|
||||
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
@@ -83,7 +132,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
func TestBuildKubernetesSecretData(t *testing.T) {
|
||||
fields := generateFields(5)
|
||||
|
||||
secretData := BuildKubernetesSecretData(fields)
|
||||
secretData := BuildKubernetesSecretData(fields, nil)
|
||||
if len(secretData) != len(fields) {
|
||||
t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData))
|
||||
}
|
||||
@@ -100,8 +149,10 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
}
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
labels := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item)
|
||||
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
|
||||
if kubeSecret.Name != strings.ToLower(name) {
|
||||
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
|
||||
}
|
||||
@@ -121,7 +172,9 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
|
||||
annotations := map[string]string{
|
||||
"annotationKey": "annotationValue",
|
||||
}
|
||||
labels := map[string]string{}
|
||||
item := onepassword.Item{}
|
||||
secretType := ""
|
||||
|
||||
item.Fields = []*onepassword.ItemField{
|
||||
{
|
||||
@@ -134,7 +187,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item)
|
||||
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
|
||||
|
||||
// Assert Secret's meta.name was fixed
|
||||
if kubeSecret.Name != expectedName {
|
||||
@@ -152,6 +205,36 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
|
||||
secretName := "tls-test-secret-name"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewClientBuilder().Build()
|
||||
secretLabels := map[string]string{}
|
||||
secretType := "kubernetes.io/tls"
|
||||
|
||||
err := CreateKubernetesSecretFromItem(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)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Secret was not created: %v", err)
|
||||
}
|
||||
|
||||
if createdSecret.Type != corev1.SecretTypeTLS {
|
||||
t.Errorf("Expected secretType to be of tyype corev1.SecretTypeTLS, got %s", string(createdSecret.Type))
|
||||
}
|
||||
}
|
||||
|
||||
func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) {
|
||||
actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
|
||||
if err != nil {
|
||||
@@ -205,7 +288,7 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
|
||||
}
|
||||
|
||||
func validLabel(v string) bool {
|
||||
if err := kubeValidate.IsDNS1123Subdomain(v); len(err) > 0 {
|
||||
if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@@ -5,62 +5,147 @@ import (
|
||||
)
|
||||
|
||||
type TestClient struct {
|
||||
GetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
|
||||
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
|
||||
GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
|
||||
GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
|
||||
CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
|
||||
GetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
GetVaultFunc func(uuid string) (*onepassword.Vault, error)
|
||||
GetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error)
|
||||
GetVaultByTitleFunc func(title string) (*onepassword.Vault, error)
|
||||
GetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error)
|
||||
GetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error)
|
||||
GetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error)
|
||||
GetItemsFunc func(vaultQuery string) ([]onepassword.Item, error)
|
||||
GetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error)
|
||||
CreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
|
||||
UpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
|
||||
DeleteItemFunc func(item *onepassword.Item, vaultQuery string) error
|
||||
DeleteItemByIDFunc func(itemUUID string, vaultQuery string) error
|
||||
DeleteItemByTitleFunc func(title string, vaultQuery string) error
|
||||
GetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error)
|
||||
GetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error)
|
||||
GetFileContentFunc func(file *onepassword.File) ([]byte, error)
|
||||
DownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error)
|
||||
LoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error
|
||||
LoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error
|
||||
LoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error
|
||||
LoadStructFunc func(config interface{}) error
|
||||
}
|
||||
|
||||
var (
|
||||
GetGetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
|
||||
DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
|
||||
DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
|
||||
DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
|
||||
DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
|
||||
DoUpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DoGetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
DoGetVaultFunc func(uuid string) (*onepassword.Vault, error)
|
||||
DoGetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error)
|
||||
DoGetVaultByTitleFunc func(title string) (*onepassword.Vault, error)
|
||||
DoGetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error)
|
||||
DoGetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error)
|
||||
DoGetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error)
|
||||
DoGetItemsFunc func(vaultQuery string) ([]onepassword.Item, error)
|
||||
DoGetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error)
|
||||
DoCreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
|
||||
DoUpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
|
||||
DoDeleteItemFunc func(item *onepassword.Item, vaultQuery string) error
|
||||
DoDeleteItemByIDFunc func(itemUUID string, vaultQuery string) error
|
||||
DoDeleteItemByTitleFunc func(title string, vaultQuery string) error
|
||||
DoGetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error)
|
||||
DoGetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error)
|
||||
DoGetFileContentFunc func(file *onepassword.File) ([]byte, error)
|
||||
DoDownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error)
|
||||
DoLoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error
|
||||
DoLoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error
|
||||
DoLoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error
|
||||
DoLoadStructFunc func(config interface{}) error
|
||||
)
|
||||
|
||||
// Do is the mock client's `Do` func
|
||||
|
||||
func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
|
||||
return GetGetVaultsFunc()
|
||||
return DoGetVaultsFunc()
|
||||
}
|
||||
|
||||
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
|
||||
return DoGetVaultsByTitleFunc(title)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
return GetGetItemFunc(uuid, vaultUUID)
|
||||
func (m *TestClient) GetVault(vaultQuery string) (*onepassword.Vault, error) {
|
||||
return DoGetVaultFunc(vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
|
||||
return DoGetItemsFunc(vaultUUID)
|
||||
func (m *TestClient) GetVaultByUUID(uuid string) (*onepassword.Vault, error) {
|
||||
return DoGetVaultByUUIDFunc(uuid)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItemsByTitle(title, vaultUUID string) ([]onepassword.Item, error) {
|
||||
return DoGetItemsByTitleFunc(title, vaultUUID)
|
||||
func (m *TestClient) GetVaultByTitle(title string) (*onepassword.Vault, error) {
|
||||
return DoGetVaultByTitleFunc(title)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoGetItemByTitleFunc(title, vaultUUID)
|
||||
func (m *TestClient) GetItem(itemQuery string, vaultQuery string) (*onepassword.Item, error) {
|
||||
return DoGetItemFunc(itemQuery, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoCreateItemFunc(item, vaultUUID)
|
||||
func (m *TestClient) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) {
|
||||
return DoGetItemByUUIDFunc(uuid, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error {
|
||||
return DoDeleteItemFunc(item, vaultUUID)
|
||||
func (m *TestClient) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) {
|
||||
return DoGetItemByTitleFunc(title, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoUpdateItemFunc(item, vaultUUID)
|
||||
func (m *TestClient) GetItems(vaultQuery string) ([]onepassword.Item, error) {
|
||||
return DoGetItemsFunc(vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) {
|
||||
return DoGetItemsByTitleFunc(title, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
|
||||
return DoCreateItemFunc(item, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
|
||||
return DoUpdateItemFunc(item, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) DeleteItem(item *onepassword.Item, vaultQuery string) error {
|
||||
return DoDeleteItemFunc(item, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) DeleteItemByID(itemUUID string, vaultQuery string) error {
|
||||
return DoDeleteItemByIDFunc(itemUUID, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) DeleteItemByTitle(title string, vaultQuery string) error {
|
||||
return DoDeleteItemByTitleFunc(title, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) {
|
||||
return DoGetFilesFunc(itemQuery, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) {
|
||||
return DoGetFileFunc(uuid, itemQuery, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) {
|
||||
return DoGetFileContentFunc(file)
|
||||
}
|
||||
|
||||
func (m *TestClient) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) {
|
||||
return DoDownloadFileFunc(file, targetDirectory, overwrite)
|
||||
}
|
||||
|
||||
func (m *TestClient) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error {
|
||||
return DoLoadStructFromItemByUUIDFunc(config, itemUUID, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error {
|
||||
return DoLoadStructFromItemByTitleFunc(config, itemTitle, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error {
|
||||
return DoLoadStructFromItemFunc(config, itemQuery, vaultQuery)
|
||||
}
|
||||
|
||||
func (m *TestClient) LoadStruct(config interface{}) error {
|
||||
return DoLoadStructFunc(config)
|
||||
}
|
||||
|
@@ -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,8 +15,8 @@ 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)
|
||||
|
@@ -23,9 +23,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(client, "../../config/connect/service.yaml", defaultNamespacedName.Namespace)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error Setting Up Connect: %v", err)
|
||||
@@ -48,9 +48,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(client, "../../config/connect/deployment.yaml", defaultNamespacedName.Namespace)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error Setting Up Connect: %v", err)
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package onepassword
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool {
|
||||
for i := 0; i < len(containers); i++ {
|
||||
@@ -13,6 +15,15 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string
|
||||
}
|
||||
}
|
||||
}
|
||||
envFromVariables := containers[i].EnvFrom
|
||||
for j := 0; j < len(envFromVariables); j++ {
|
||||
if envFromVariables[j].SecretRef != nil {
|
||||
_, ok := secrets[envFromVariables[j].SecretRef.Name]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -28,6 +39,15 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st
|
||||
}
|
||||
}
|
||||
}
|
||||
envFromVariables := containers[i].EnvFrom
|
||||
for j := 0; j < len(envFromVariables); j++ {
|
||||
if envFromVariables[j].SecretRef != nil {
|
||||
secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name]
|
||||
if ok {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return updatedDeploymentSecrets
|
||||
}
|
||||
|
@@ -4,12 +4,13 @@ import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestAreContainersUsingSecrets(t *testing.T) {
|
||||
func TestAreContainersUsingSecretsFromEnv(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
@@ -18,7 +19,26 @@ func TestAreContainersUsingSecrets(t *testing.T) {
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
containers := generateContainers(containerSecretNames)
|
||||
containers := generateContainersWithSecretRefsFromEnv(containerSecretNames)
|
||||
|
||||
if !AreContainersUsingSecrets(containers, secretNamesToSearch) {
|
||||
t.Errorf("Expected that containers were using secrets but they were not detected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAreContainersUsingSecretsFromEnvFrom(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
"onepassword-database-secret",
|
||||
"onepassword-api-key",
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames)
|
||||
|
||||
if !AreContainersUsingSecrets(containers, secretNamesToSearch) {
|
||||
t.Errorf("Expected that containers were using secrets but they were not detected.")
|
||||
@@ -27,17 +47,39 @@ func TestAreContainersUsingSecrets(t *testing.T) {
|
||||
|
||||
func TestAreContainersNotUsingSecrets(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{
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
containers := generateContainers(containerSecretNames)
|
||||
containers := generateContainersWithSecretRefsFromEnv(containerSecretNames)
|
||||
|
||||
if AreContainersUsingSecrets(containers, secretNamesToSearch) {
|
||||
t.Errorf("Expected that containers were not using secrets but they were detected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUpdatedContainerSecretsParsesEnvFromEnv(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {ObjectMeta: metav1.ObjectMeta{Name: "onepassword-api-key"}},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
"onepassword-api-key",
|
||||
}
|
||||
|
||||
containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames)
|
||||
|
||||
updatedDeploymentSecrets := map[string]*corev1.Secret{}
|
||||
updatedDeploymentSecrets = AppendUpdatedContainerSecrets(containers, secretNamesToSearch, updatedDeploymentSecrets)
|
||||
|
||||
secretKeyName := "onepassword-api-key"
|
||||
|
||||
if updatedDeploymentSecrets[secretKeyName] != secretNamesToSearch[secretKeyName] {
|
||||
t.Errorf("Expected that updated Secret from envfrom is found.")
|
||||
}
|
||||
}
|
||||
|
@@ -9,8 +9,8 @@ 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": {},
|
||||
}
|
||||
|
||||
volumeSecretNames := []string{
|
||||
@@ -28,8 +28,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{
|
||||
@@ -39,7 +39,7 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) {
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
deployment.Spec.Template.Spec.Containers = generateContainers(containerSecretNames)
|
||||
deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames)
|
||||
if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) {
|
||||
t.Errorf("Expected that deployment was using secrets but they were not detected.")
|
||||
}
|
||||
@@ -47,8 +47,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{}
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
@@ -30,6 +31,14 @@ func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*one
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range item.Files {
|
||||
_, err := opConnectClient.GetFileContent(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
|
@@ -17,8 +17,7 @@ func generateVolumes(names []string) []corev1.Volume {
|
||||
}
|
||||
return volumes
|
||||
}
|
||||
|
||||
func generateContainers(names []string) []corev1.Container {
|
||||
func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container {
|
||||
containers := []corev1.Container{}
|
||||
for i := 0; i < len(names); i++ {
|
||||
container := corev1.Container{
|
||||
@@ -40,3 +39,16 @@ func generateContainers(names []string) []corev1.Container {
|
||||
}
|
||||
return containers
|
||||
}
|
||||
|
||||
func generateContainersWithSecretRefsFromEnvFrom(names []string) []corev1.Container {
|
||||
containers := []corev1.Container{}
|
||||
for i := 0; i < len(names); i++ {
|
||||
container := corev1.Container{
|
||||
EnvFrom: []corev1.EnvFromSource{
|
||||
{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: names[i]}}},
|
||||
},
|
||||
}
|
||||
containers = append(containers, container)
|
||||
}
|
||||
return containers
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
@@ -89,9 +90,10 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
|
||||
|
||||
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) {
|
||||
log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace))
|
||||
deployment.Spec.Template.Annotations = map[string]string{
|
||||
RestartAnnotation: time.Now().String(),
|
||||
if deployment.Spec.Template.Annotations == nil {
|
||||
deployment.Spec.Template.Annotations = map[string]string{}
|
||||
}
|
||||
deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String()
|
||||
err := h.client.Update(context.Background(), deployment)
|
||||
if err != nil {
|
||||
log.Error(err, "Problem restarting deployment")
|
||||
@@ -116,23 +118,37 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
|
||||
continue
|
||||
}
|
||||
|
||||
item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation])
|
||||
OnePasswordItemPath := h.getPathFromOnePasswordItem(secret)
|
||||
|
||||
item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to retrieve item: %v", err)
|
||||
log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
itemVersion := fmt.Sprint(item.Version)
|
||||
if currentVersion != itemVersion {
|
||||
itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
|
||||
|
||||
if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString {
|
||||
if isItemLockedForForcedRestarts(item) {
|
||||
log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName()))
|
||||
secret.Annotations[VersionAnnotation] = itemVersion
|
||||
h.client.Update(context.Background(), &secret)
|
||||
secret.Annotations[ItemPathAnnotation] = itemPathString
|
||||
if err := h.client.Update(context.Background(), &secret); err != nil {
|
||||
log.Error(err, "failed to update secret %s annotations to version %d: %s", secret.Name, itemVersion, err)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
|
||||
secret.Annotations[VersionAnnotation] = itemVersion
|
||||
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item)
|
||||
h.client.Update(context.Background(), updatedSecret)
|
||||
secret.Annotations[ItemPathAnnotation] = itemPathString
|
||||
secret.Data = kubeSecrets.BuildKubernetesSecretData(item.Fields, item.Files)
|
||||
log.Info(fmt.Sprintf("New secret path: %v and version: %v", secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation]))
|
||||
if err := h.client.Update(context.Background(), &secret); err != nil {
|
||||
log.Error(err, "failed to update secret %s to version %d: %s", secret.Name, itemVersion, err)
|
||||
continue
|
||||
}
|
||||
if updatedSecrets[secret.Namespace] == nil {
|
||||
updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)
|
||||
}
|
||||
@@ -176,6 +192,22 @@ func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string
|
||||
return namespacesMap, nil
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string {
|
||||
onePasswordItem := &onepasswordv1.OnePasswordItem{}
|
||||
|
||||
// Search for our original OnePasswordItem if it exists
|
||||
err := h.client.Get(context.TODO(), client.ObjectKey{
|
||||
Namespace: secret.Namespace,
|
||||
Name: secret.Name}, onePasswordItem)
|
||||
|
||||
if err == nil {
|
||||
return onePasswordItem.Spec.ItemPath
|
||||
}
|
||||
|
||||
// If we can't find the OnePassword Item we'll just return the annotation from the secret item.
|
||||
return secret.Annotations[ItemPathAnnotation]
|
||||
}
|
||||
|
||||
func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
|
||||
restartDeployment := secret.Annotations[RestartDeploymentsAnnotation]
|
||||
//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
|
||||
|
@@ -122,6 +122,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 +238,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 +348,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 +420,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 +494,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 +568,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 +648,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 +724,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{
|
||||
{
|
||||
@@ -776,10 +800,10 @@ 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) {
|
||||
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
|
||||
@@ -829,6 +853,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 +871,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))
|
||||
|
||||
|
@@ -8,8 +8,8 @@ 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": {},
|
||||
}
|
||||
|
||||
volumeSecretNames := []string{
|
||||
@@ -27,8 +27,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{
|
||||
|
63
pkg/utils/k8sutil.go
Normal file
63
pkg/utils/k8sutil.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
var ForceRunModeEnv = "OSDK_FORCE_RUN_MODE"
|
||||
|
||||
type RunModeType string
|
||||
|
||||
const (
|
||||
LocalRunMode RunModeType = "local"
|
||||
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")
|
||||
|
||||
// ErrRunLocal indicates that the operator is set to run in local mode (this error
|
||||
// is returned by functions that only work on operators running in cluster mode)
|
||||
var ErrRunLocal = fmt.Errorf("operator run mode forced to local")
|
||||
|
||||
// GetOperatorNamespace returns the namespace the operator should be running in.
|
||||
func GetOperatorNamespace() (string, error) {
|
||||
if isRunModeLocal() {
|
||||
return "", ErrRunLocal
|
||||
}
|
||||
nsBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", ErrNoNamespace
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
ns := strings.TrimSpace(string(nsBytes))
|
||||
log.V(1).Info("Found namespace", "Namespace", ns)
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func isRunModeLocal() bool {
|
||||
return os.Getenv(ForceRunModeEnv) == string(LocalRunMode)
|
||||
}
|
5
tools.go
5
tools.go
@@ -1,5 +0,0 @@
|
||||
// +build tools
|
||||
|
||||
// Place any runtime dependencies as imports in this file.
|
||||
// Go modules will be forced to download and install them.
|
||||
package tools
|
12
vendor/cloud.google.com/go/compute/metadata/.repo-metadata.json
generated
vendored
12
vendor/cloud.google.com/go/compute/metadata/.repo-metadata.json
generated
vendored
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "metadata",
|
||||
"name_pretty": "Google Compute Engine Metadata API",
|
||||
"product_documentation": "https://cloud.google.com/compute/docs/storing-retrieving-metadata",
|
||||
"client_documentation": "https://godoc.org/cloud.google.com/go/compute/metadata",
|
||||
"release_level": "ga",
|
||||
"language": "go",
|
||||
"repo": "googleapis/google-cloud-go",
|
||||
"distribution_name": "cloud.google.com/go/compute/metadata",
|
||||
"api_id": "compute:metadata",
|
||||
"requires_billing": false
|
||||
}
|
526
vendor/cloud.google.com/go/compute/metadata/metadata.go
generated
vendored
526
vendor/cloud.google.com/go/compute/metadata/metadata.go
generated
vendored
@@ -1,526 +0,0 @@
|
||||
// Copyright 2014 Google LLC
|
||||
//
|
||||
// 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 metadata provides access to Google Compute Engine (GCE)
|
||||
// metadata and API service accounts.
|
||||
//
|
||||
// This package is a wrapper around the GCE metadata service,
|
||||
// as documented at https://developers.google.com/compute/docs/metadata.
|
||||
package metadata // import "cloud.google.com/go/compute/metadata"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// metadataIP is the documented metadata server IP address.
|
||||
metadataIP = "169.254.169.254"
|
||||
|
||||
// metadataHostEnv is the environment variable specifying the
|
||||
// GCE metadata hostname. If empty, the default value of
|
||||
// metadataIP ("169.254.169.254") is used instead.
|
||||
// This is variable name is not defined by any spec, as far as
|
||||
// I know; it was made up for the Go package.
|
||||
metadataHostEnv = "GCE_METADATA_HOST"
|
||||
|
||||
userAgent = "gcloud-golang/0.1"
|
||||
)
|
||||
|
||||
type cachedValue struct {
|
||||
k string
|
||||
trim bool
|
||||
mu sync.Mutex
|
||||
v string
|
||||
}
|
||||
|
||||
var (
|
||||
projID = &cachedValue{k: "project/project-id", trim: true}
|
||||
projNum = &cachedValue{k: "project/numeric-project-id", trim: true}
|
||||
instID = &cachedValue{k: "instance/id", trim: true}
|
||||
)
|
||||
|
||||
var (
|
||||
defaultClient = &Client{hc: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 2 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
ResponseHeaderTimeout: 2 * time.Second,
|
||||
},
|
||||
}}
|
||||
subscribeClient = &Client{hc: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 2 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
},
|
||||
}}
|
||||
)
|
||||
|
||||
// NotDefinedError is returned when requested metadata is not defined.
|
||||
//
|
||||
// The underlying string is the suffix after "/computeMetadata/v1/".
|
||||
//
|
||||
// This error is not returned if the value is defined to be the empty
|
||||
// string.
|
||||
type NotDefinedError string
|
||||
|
||||
func (suffix NotDefinedError) Error() string {
|
||||
return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
|
||||
}
|
||||
|
||||
func (c *cachedValue) get(cl *Client) (v string, err error) {
|
||||
defer c.mu.Unlock()
|
||||
c.mu.Lock()
|
||||
if c.v != "" {
|
||||
return c.v, nil
|
||||
}
|
||||
if c.trim {
|
||||
v, err = cl.getTrimmed(c.k)
|
||||
} else {
|
||||
v, err = cl.Get(c.k)
|
||||
}
|
||||
if err == nil {
|
||||
c.v = v
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
onGCEOnce sync.Once
|
||||
onGCE bool
|
||||
)
|
||||
|
||||
// OnGCE reports whether this process is running on Google Compute Engine.
|
||||
func OnGCE() bool {
|
||||
onGCEOnce.Do(initOnGCE)
|
||||
return onGCE
|
||||
}
|
||||
|
||||
func initOnGCE() {
|
||||
onGCE = testOnGCE()
|
||||
}
|
||||
|
||||
func testOnGCE() bool {
|
||||
// The user explicitly said they're on GCE, so trust them.
|
||||
if os.Getenv(metadataHostEnv) != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
resc := make(chan bool, 2)
|
||||
|
||||
// Try two strategies in parallel.
|
||||
// See https://github.com/googleapis/google-cloud-go/issues/194
|
||||
go func() {
|
||||
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
res, err := defaultClient.hc.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
resc <- false
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
resc <- res.Header.Get("Metadata-Flavor") == "Google"
|
||||
}()
|
||||
|
||||
go func() {
|
||||
addrs, err := net.LookupHost("metadata.google.internal")
|
||||
if err != nil || len(addrs) == 0 {
|
||||
resc <- false
|
||||
return
|
||||
}
|
||||
resc <- strsContains(addrs, metadataIP)
|
||||
}()
|
||||
|
||||
tryHarder := systemInfoSuggestsGCE()
|
||||
if tryHarder {
|
||||
res := <-resc
|
||||
if res {
|
||||
// The first strategy succeeded, so let's use it.
|
||||
return true
|
||||
}
|
||||
// Wait for either the DNS or metadata server probe to
|
||||
// contradict the other one and say we are running on
|
||||
// GCE. Give it a lot of time to do so, since the system
|
||||
// info already suggests we're running on a GCE BIOS.
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case res = <-resc:
|
||||
return res
|
||||
case <-timer.C:
|
||||
// Too slow. Who knows what this system is.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// There's no hint from the system info that we're running on
|
||||
// GCE, so use the first probe's result as truth, whether it's
|
||||
// true or false. The goal here is to optimize for speed for
|
||||
// users who are NOT running on GCE. We can't assume that
|
||||
// either a DNS lookup or an HTTP request to a blackholed IP
|
||||
// address is fast. Worst case this should return when the
|
||||
// metaClient's Transport.ResponseHeaderTimeout or
|
||||
// Transport.Dial.Timeout fires (in two seconds).
|
||||
return <-resc
|
||||
}
|
||||
|
||||
// systemInfoSuggestsGCE reports whether the local system (without
|
||||
// doing network requests) suggests that we're running on GCE. If this
|
||||
// returns true, testOnGCE tries a bit harder to reach its metadata
|
||||
// server.
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
// We don't have any non-Linux clues available, at least yet.
|
||||
return false
|
||||
}
|
||||
slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name")
|
||||
name := strings.TrimSpace(string(slurp))
|
||||
return name == "Google" || name == "Google Compute Engine"
|
||||
}
|
||||
|
||||
// Subscribe calls Client.Subscribe on a client designed for subscribing (one with no
|
||||
// ResponseHeaderTimeout).
|
||||
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
return subscribeClient.Subscribe(suffix, fn)
|
||||
}
|
||||
|
||||
// Get calls Client.Get on the default client.
|
||||
func Get(suffix string) (string, error) { return defaultClient.Get(suffix) }
|
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
func ProjectID() (string, error) { return defaultClient.ProjectID() }
|
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
func NumericProjectID() (string, error) { return defaultClient.NumericProjectID() }
|
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
func InternalIP() (string, error) { return defaultClient.InternalIP() }
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
func ExternalIP() (string, error) { return defaultClient.ExternalIP() }
|
||||
|
||||
// Email calls Client.Email on the default client.
|
||||
func Email(serviceAccount string) (string, error) { return defaultClient.Email(serviceAccount) }
|
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func Hostname() (string, error) { return defaultClient.Hostname() }
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func InstanceTags() ([]string, error) { return defaultClient.InstanceTags() }
|
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
func InstanceID() (string, error) { return defaultClient.InstanceID() }
|
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
func InstanceName() (string, error) { return defaultClient.InstanceName() }
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
func Zone() (string, error) { return defaultClient.Zone() }
|
||||
|
||||
// InstanceAttributes calls Client.InstanceAttributes on the default client.
|
||||
func InstanceAttributes() ([]string, error) { return defaultClient.InstanceAttributes() }
|
||||
|
||||
// ProjectAttributes calls Client.ProjectAttributes on the default client.
|
||||
func ProjectAttributes() ([]string, error) { return defaultClient.ProjectAttributes() }
|
||||
|
||||
// InstanceAttributeValue calls Client.InstanceAttributeValue on the default client.
|
||||
func InstanceAttributeValue(attr string) (string, error) {
|
||||
return defaultClient.InstanceAttributeValue(attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue calls Client.ProjectAttributeValue on the default client.
|
||||
func ProjectAttributeValue(attr string) (string, error) {
|
||||
return defaultClient.ProjectAttributeValue(attr)
|
||||
}
|
||||
|
||||
// Scopes calls Client.Scopes on the default client.
|
||||
func Scopes(serviceAccount string) ([]string, error) { return defaultClient.Scopes(serviceAccount) }
|
||||
|
||||
func strsContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Client provides metadata.
|
||||
type Client struct {
|
||||
hc *http.Client
|
||||
}
|
||||
|
||||
// NewClient returns a Client that can be used to fetch metadata. All HTTP requests
|
||||
// will use the given http.Client instead of the default client.
|
||||
func NewClient(c *http.Client) *Client {
|
||||
return &Client{hc: c}
|
||||
}
|
||||
|
||||
// getETag returns a value from the metadata service as well as the associated ETag.
|
||||
// This func is otherwise equivalent to Get.
|
||||
func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
||||
// Using a fixed IP makes it very difficult to spoof the metadata service in
|
||||
// a container, which is an important use-case for local testing of cloud
|
||||
// deployments. To enable spoofing of the metadata service, the environment
|
||||
// variable GCE_METADATA_HOST is first inspected to decide where metadata
|
||||
// requests shall go.
|
||||
host := os.Getenv(metadataHostEnv)
|
||||
if host == "" {
|
||||
// Using 169.254.169.254 instead of "metadata" here because Go
|
||||
// binaries built with the "netgo" tag and without cgo won't
|
||||
// know the search suffix for "metadata" is
|
||||
// ".google.internal", and this IP address is documented as
|
||||
// being stable anyway.
|
||||
host = metadataIP
|
||||
}
|
||||
u := "http://" + host + "/computeMetadata/v1/" + suffix
|
||||
req, _ := http.NewRequest("GET", u, nil)
|
||||
req.Header.Set("Metadata-Flavor", "Google")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
res, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
return "", "", NotDefinedError(suffix)
|
||||
}
|
||||
all, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return "", "", &Error{Code: res.StatusCode, Message: string(all)}
|
||||
}
|
||||
return string(all), res.Header.Get("Etag"), nil
|
||||
}
|
||||
|
||||
// Get returns a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
//
|
||||
// If the GCE_METADATA_HOST environment variable is not defined, a default of
|
||||
// 169.254.169.254 will be used instead.
|
||||
//
|
||||
// If the requested metadata is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
func (c *Client) Get(suffix string) (string, error) {
|
||||
val, _, err := c.getETag(suffix)
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (c *Client) getTrimmed(suffix string) (s string, err error) {
|
||||
s, err = c.Get(suffix)
|
||||
s = strings.TrimSpace(s)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) lines(suffix string) ([]string, error) {
|
||||
j, err := c.Get(suffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := strings.Split(strings.TrimSpace(j), "\n")
|
||||
for i := range s {
|
||||
s[i] = strings.TrimSpace(s[i])
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
func (c *Client) ProjectID() (string, error) { return projID.get(c) }
|
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
func (c *Client) NumericProjectID() (string, error) { return projNum.get(c) }
|
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
func (c *Client) InstanceID() (string, error) { return instID.get(c) }
|
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
func (c *Client) InternalIP() (string, error) {
|
||||
return c.getTrimmed("instance/network-interfaces/0/ip")
|
||||
}
|
||||
|
||||
// Email returns the email address associated with the service account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
func (c *Client) Email(serviceAccount string) (string, error) {
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.getTrimmed("instance/service-accounts/" + serviceAccount + "/email")
|
||||
}
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
func (c *Client) ExternalIP() (string, error) {
|
||||
return c.getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip")
|
||||
}
|
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func (c *Client) Hostname() (string, error) {
|
||||
return c.getTrimmed("instance/hostname")
|
||||
}
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func (c *Client) InstanceTags() ([]string, error) {
|
||||
var s []string
|
||||
j, err := c.Get("instance/tags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
func (c *Client) InstanceName() (string, error) {
|
||||
host, err := c.Hostname()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Split(host, ".")[0], nil
|
||||
}
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
func (c *Client) Zone() (string, error) {
|
||||
zone, err := c.getTrimmed("instance/zone")
|
||||
// zone is of the form "projects/<projNum>/zones/<zoneName>".
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return zone[strings.LastIndex(zone, "/")+1:], nil
|
||||
}
|
||||
|
||||
// InstanceAttributes returns the list of user-defined attributes,
|
||||
// assigned when initially creating a GCE VM instance. The value of an
|
||||
// attribute can be obtained with InstanceAttributeValue.
|
||||
func (c *Client) InstanceAttributes() ([]string, error) { return c.lines("instance/attributes/") }
|
||||
|
||||
// ProjectAttributes returns the list of user-defined attributes
|
||||
// applying to the project as a whole, not just this VM. The value of
|
||||
// an attribute can be obtained with ProjectAttributeValue.
|
||||
func (c *Client) ProjectAttributes() ([]string, error) { return c.lines("project/attributes/") }
|
||||
|
||||
// InstanceAttributeValue returns the value of the provided VM
|
||||
// instance attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) InstanceAttributeValue(attr string) (string, error) {
|
||||
return c.Get("instance/attributes/" + attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue returns the value of the provided
|
||||
// project attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) ProjectAttributeValue(attr string) (string, error) {
|
||||
return c.Get("project/attributes/" + attr)
|
||||
}
|
||||
|
||||
// Scopes returns the service account scopes for the given account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
func (c *Client) Scopes(serviceAccount string) ([]string, error) {
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.lines("instance/service-accounts/" + serviceAccount + "/scopes")
|
||||
}
|
||||
|
||||
// Subscribe subscribes to a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
// The suffix may contain query parameters.
|
||||
//
|
||||
// Subscribe calls fn with the latest metadata value indicated by the provided
|
||||
// suffix. If the metadata value is deleted, fn is called with the empty string
|
||||
// and ok false. Subscribe blocks until fn returns a non-nil error or the value
|
||||
// is deleted. Subscribe returns the error value returned from the last call to
|
||||
// fn, which may be nil when ok == false.
|
||||
func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
const failedSubscribeSleep = time.Second * 5
|
||||
|
||||
// First check to see if the metadata value exists at all.
|
||||
val, lastETag, err := c.getETag(suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(val, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ok := true
|
||||
if strings.ContainsRune(suffix, '?') {
|
||||
suffix += "&wait_for_change=true&last_etag="
|
||||
} else {
|
||||
suffix += "?wait_for_change=true&last_etag="
|
||||
}
|
||||
for {
|
||||
val, etag, err := c.getETag(suffix + url.QueryEscape(lastETag))
|
||||
if err != nil {
|
||||
if _, deleted := err.(NotDefinedError); !deleted {
|
||||
time.Sleep(failedSubscribeSleep)
|
||||
continue // Retry on other errors.
|
||||
}
|
||||
ok = false
|
||||
}
|
||||
lastETag = etag
|
||||
|
||||
if err := fn(val, ok); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error contains an error response from the server.
|
||||
type Error struct {
|
||||
// Code is the HTTP response status code.
|
||||
Code int
|
||||
// Message is the server response message.
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("compute: Received %d `%s`", e.Code, e.Message)
|
||||
}
|
21
vendor/github.com/1Password/connect-sdk-go/LICENSE
generated
vendored
Normal file
21
vendor/github.com/1Password/connect-sdk-go/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 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.
|
657
vendor/github.com/1Password/connect-sdk-go/connect/client.go
generated
vendored
657
vendor/github.com/1Password/connect-sdk-go/connect/client.go
generated
vendored
@@ -9,29 +9,53 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/opentracing/opentracing-go/ext"
|
||||
jaegerClientConfig "github.com/uber/jaeger-client-go/config"
|
||||
"github.com/uber/jaeger-client-go/zipkin"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUserAgent = "connect-sdk-go/%s"
|
||||
)
|
||||
|
||||
var (
|
||||
vaultUUIDError = fmt.Errorf("malformed vault uuid provided")
|
||||
itemUUIDError = fmt.Errorf("malformed item uuid provided")
|
||||
fileUUIDError = fmt.Errorf("malformed file uuid provided")
|
||||
)
|
||||
|
||||
// Client Represents an available 1Password Connect API to connect to
|
||||
type Client interface {
|
||||
GetVaults() ([]onepassword.Vault, error)
|
||||
GetVault(uuid string) (*onepassword.Vault, error)
|
||||
GetVaultByUUID(uuid string) (*onepassword.Vault, error)
|
||||
GetVaultByTitle(title string) (*onepassword.Vault, error)
|
||||
GetVaultsByTitle(uuid string) ([]onepassword.Vault, error)
|
||||
GetItem(uuid string, vaultUUID string) (*onepassword.Item, error)
|
||||
GetItems(vaultUUID string) ([]onepassword.Item, error)
|
||||
GetItemsByTitle(title string, vaultUUID string) ([]onepassword.Item, error)
|
||||
GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error)
|
||||
CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DeleteItem(item *onepassword.Item, vaultUUID string) error
|
||||
GetItems(vaultQuery string) ([]onepassword.Item, error)
|
||||
GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error)
|
||||
GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error)
|
||||
GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error)
|
||||
GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error)
|
||||
CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
|
||||
UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
|
||||
DeleteItem(item *onepassword.Item, vaultQuery string) error
|
||||
DeleteItemByID(itemUUID string, vaultQuery string) error
|
||||
DeleteItemByTitle(title string, vaultQuery string) error
|
||||
GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error)
|
||||
GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error)
|
||||
GetFileContent(file *onepassword.File) ([]byte, error)
|
||||
DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error)
|
||||
LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error
|
||||
LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error
|
||||
LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error
|
||||
LoadStruct(config interface{}) error
|
||||
}
|
||||
|
||||
type httpClient interface {
|
||||
@@ -112,23 +136,70 @@ func (rs *restClient) GetVaults() ([]onepassword.Vault, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unable to retrieve vaults. Receieved %q for %q", response.Status, vaultURL)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vaults := []onepassword.Vault{}
|
||||
if err := json.Unmarshal(body, &vaults); err != nil {
|
||||
var vaults []onepassword.Vault
|
||||
if err := parseResponse(response, http.StatusOK, &vaults); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vaults, nil
|
||||
}
|
||||
|
||||
// GetVault Get a vault based on its name or ID
|
||||
func (rs *restClient) GetVault(vaultQuery string) (*onepassword.Vault, error) {
|
||||
span := rs.tracer.StartSpan("GetVault")
|
||||
defer span.Finish()
|
||||
|
||||
if vaultQuery == "" {
|
||||
return nil, fmt.Errorf("Please provide either the vault name or its ID.")
|
||||
}
|
||||
if !isValidUUID(vaultQuery) {
|
||||
return rs.GetVaultByTitle(vaultQuery)
|
||||
}
|
||||
return rs.GetVaultByUUID(vaultQuery)
|
||||
}
|
||||
|
||||
func (rs *restClient) GetVaultByUUID(uuid string) (*onepassword.Vault, error) {
|
||||
if !isValidUUID(uuid) {
|
||||
return nil, vaultUUIDError
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("GetVaultByUUID")
|
||||
defer span.Finish()
|
||||
|
||||
vaultURL := fmt.Sprintf("/v1/vaults/%s", uuid)
|
||||
request, err := rs.buildRequest(http.MethodGet, vaultURL, http.NoBody, span)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := rs.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var vault onepassword.Vault
|
||||
if err := parseResponse(response, http.StatusOK, &vault); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &vault, nil
|
||||
}
|
||||
|
||||
func (rs *restClient) GetVaultByTitle(vaultName string) (*onepassword.Vault, error) {
|
||||
span := rs.tracer.StartSpan("GetVaultByTitle")
|
||||
defer span.Finish()
|
||||
|
||||
vaults, err := rs.GetVaultsByTitle(vaultName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(vaults) != 1 {
|
||||
return nil, fmt.Errorf("Found %d vaults with title %q", len(vaults), vaultName)
|
||||
}
|
||||
|
||||
return &vaults[0], nil
|
||||
}
|
||||
|
||||
func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
|
||||
span := rs.tracer.StartSpan("GetVaultsByTitle")
|
||||
defer span.Finish()
|
||||
@@ -145,28 +216,56 @@ func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unable to retrieve vaults. Receieved %q for %q", response.Status, itemURL)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vaults := []onepassword.Vault{}
|
||||
if err := json.Unmarshal(body, &vaults); err != nil {
|
||||
var vaults []onepassword.Vault
|
||||
if err := parseResponse(response, http.StatusOK, &vaults); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vaults, nil
|
||||
}
|
||||
|
||||
// GetItem Get a specific Item from the 1Password Connect API
|
||||
func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
func (rs *restClient) getVaultUUID(vaultQuery string) (string, error) {
|
||||
if vaultQuery == "" {
|
||||
return "", fmt.Errorf("Please provide either the vault name or its ID.")
|
||||
}
|
||||
if isValidUUID(vaultQuery) {
|
||||
return vaultQuery, nil
|
||||
}
|
||||
vault, err := rs.GetVaultByTitle(vaultQuery)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return vault.ID, nil
|
||||
}
|
||||
|
||||
// GetItem Get a specific Item from the 1Password Connect API by either title or UUID
|
||||
func (rs *restClient) GetItem(itemQuery string, vaultQuery string) (*onepassword.Item, error) {
|
||||
span := rs.tracer.StartSpan("GetItem")
|
||||
defer span.Finish()
|
||||
|
||||
if itemQuery == "" {
|
||||
return nil, fmt.Errorf("Please provide either the item name or its ID.")
|
||||
}
|
||||
if !isValidUUID(itemQuery) {
|
||||
return rs.GetItemByTitle(itemQuery, vaultQuery)
|
||||
}
|
||||
return rs.GetItemByUUID(itemQuery, vaultQuery)
|
||||
}
|
||||
|
||||
// GetItemByUUID Get a specific Item from the 1Password Connect API by its UUID
|
||||
func (rs *restClient) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) {
|
||||
if !isValidUUID(uuid) {
|
||||
return nil, itemUUIDError
|
||||
}
|
||||
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("GetItemByUUID")
|
||||
defer span.Finish()
|
||||
|
||||
itemURL := fmt.Sprintf("/v1/vaults/%s/items/%s", vaultUUID, uuid)
|
||||
request, err := rs.buildRequest(http.MethodGet, itemURL, http.NoBody, span)
|
||||
if err != nil {
|
||||
@@ -177,25 +276,20 @@ func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := onepassword.Item{}
|
||||
if err := json.Unmarshal(body, &item); err != nil {
|
||||
var item onepassword.Item
|
||||
if err := parseResponse(response, http.StatusOK, &item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (rs *restClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) {
|
||||
func (rs *restClient) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) {
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("GetItemByTitle")
|
||||
defer span.Finish()
|
||||
items, err := rs.GetItemsByTitle(title, vaultUUID)
|
||||
@@ -207,10 +301,15 @@ func (rs *restClient) GetItemByTitle(title string, vaultUUID string) (*onepasswo
|
||||
return nil, fmt.Errorf("Found %d item(s) in vault %q with title %q", len(items), vaultUUID, title)
|
||||
}
|
||||
|
||||
return rs.GetItem(items[0].ID, items[0].Vault.ID)
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
func (rs *restClient) GetItemsByTitle(title string, vaultUUID string) ([]onepassword.Item, error) {
|
||||
func (rs *restClient) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) {
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("GetItemsByTitle")
|
||||
defer span.Finish()
|
||||
|
||||
@@ -226,24 +325,29 @@ func (rs *restClient) GetItemsByTitle(title string, vaultUUID string) ([]onepass
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
var itemSummaries []onepassword.Item
|
||||
if err := parseResponse(response, http.StatusOK, &itemSummaries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := []onepassword.Item{}
|
||||
if err := json.Unmarshal(body, &items); err != nil {
|
||||
return nil, err
|
||||
items := make([]onepassword.Item, len(itemSummaries))
|
||||
for i, itemSummary := range itemSummaries {
|
||||
tempItem, err := rs.GetItem(itemSummary.ID, itemSummary.Vault.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items[i] = *tempItem
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (rs *restClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
|
||||
func (rs *restClient) GetItems(vaultQuery string) ([]onepassword.Item, error) {
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("GetItems")
|
||||
defer span.Finish()
|
||||
|
||||
@@ -258,25 +362,35 @@ func (rs *restClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unable to retrieve items. Receieved %q for %q", response.Status, itemURL)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := []onepassword.Item{}
|
||||
if err := json.Unmarshal(body, &items); err != nil {
|
||||
var items []onepassword.Item
|
||||
if err := parseResponse(response, http.StatusOK, &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (rs *restClient) getItemUUID(itemQuery, vaultQuery string) (string, error) {
|
||||
if itemQuery == "" {
|
||||
return "", fmt.Errorf("Please provide either the item name or its ID.")
|
||||
}
|
||||
if isValidUUID(itemQuery) {
|
||||
return itemQuery, nil
|
||||
}
|
||||
item, err := rs.GetItemByTitle(itemQuery, vaultQuery)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return item.ID, nil
|
||||
}
|
||||
|
||||
// CreateItem Create a new item in a specified vault
|
||||
func (rs *restClient) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
|
||||
func (rs *restClient) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("CreateItem")
|
||||
defer span.Finish()
|
||||
|
||||
@@ -296,17 +410,8 @@ func (rs *restClient) CreateItem(item *onepassword.Item, vaultUUID string) (*one
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unable to create item. Receieved %q for %q", response.Status, itemURL)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newItem := onepassword.Item{}
|
||||
if err := json.Unmarshal(body, &newItem); err != nil {
|
||||
var newItem onepassword.Item
|
||||
if err := parseResponse(response, http.StatusOK, &newItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -334,17 +439,8 @@ func (rs *restClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*one
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unable to update item. Receieved %q for %q", response.Status, itemURL)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newItem := onepassword.Item{}
|
||||
if err := json.Unmarshal(body, &newItem); err != nil {
|
||||
var newItem onepassword.Item
|
||||
if err := parseResponse(response, http.StatusOK, &newItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -367,13 +463,213 @@ func (rs *restClient) DeleteItem(item *onepassword.Item, vaultUUID string) error
|
||||
return err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL)
|
||||
if err := parseResponse(response, http.StatusNoContent, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteItemByID Delete a new item in a specified vault, specifying the item's uuid
|
||||
func (rs *restClient) DeleteItemByID(itemUUID string, vaultQuery string) error {
|
||||
if !isValidUUID(itemUUID) {
|
||||
return itemUUIDError
|
||||
}
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("DeleteItemByID")
|
||||
defer span.Finish()
|
||||
|
||||
itemURL := fmt.Sprintf("/v1/vaults/%s/items/%s", vaultUUID, itemUUID)
|
||||
request, err := rs.buildRequest(http.MethodDelete, itemURL, http.NoBody, span)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := rs.client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := parseResponse(response, http.StatusNoContent, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteItemByTitle Delete a new item in a specified vault, specifying the item's title
|
||||
func (rs *restClient) DeleteItemByTitle(title string, vaultQuery string) error {
|
||||
span := rs.tracer.StartSpan("DeleteItemByTitle")
|
||||
defer span.Finish()
|
||||
|
||||
item, err := rs.GetItemByTitle(title, vaultQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rs.DeleteItem(item, item.Vault.ID)
|
||||
}
|
||||
|
||||
func (rs *restClient) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) {
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
itemUUID, err := rs.getItemUUID(itemQuery, vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("GetFiles")
|
||||
defer span.Finish()
|
||||
|
||||
jsonURL := fmt.Sprintf("/v1/vaults/%s/items/%s/files", vaultUUID, itemUUID)
|
||||
request, err := rs.buildRequest(http.MethodGet, jsonURL, http.NoBody, span)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := rs.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var files []onepassword.File
|
||||
if err := parseResponse(response, http.StatusOK, &files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// GetFile Get a specific File in a specified item.
|
||||
// This does not include the file contents. Call GetFileContent() to load the file's content.
|
||||
func (rs *restClient) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) {
|
||||
if !isValidUUID(uuid) {
|
||||
return nil, fileUUIDError
|
||||
}
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
itemUUID, err := rs.getItemUUID(itemQuery, vaultQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
span := rs.tracer.StartSpan("GetFile")
|
||||
defer span.Finish()
|
||||
|
||||
itemURL := fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s", vaultUUID, itemUUID, uuid)
|
||||
request, err := rs.buildRequest(http.MethodGet, itemURL, http.NoBody, span)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := rs.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var file onepassword.File
|
||||
if err := parseResponse(response, http.StatusOK, &file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
// GetFileContent retrieves the file's content.
|
||||
// If the file's content have previously been fetched, those contents are returned without making another request.
|
||||
func (rs *restClient) GetFileContent(file *onepassword.File) ([]byte, error) {
|
||||
if content, err := file.Content(); err == nil {
|
||||
return content, nil
|
||||
}
|
||||
response, err := rs.retrieveDocumentContent(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := readResponseBody(response, http.StatusOK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file.SetContent(content)
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (rs *restClient) DownloadFile(file *onepassword.File, targetDirectory string, overwriteIfExists bool) (string, error) {
|
||||
response, err := rs.retrieveDocumentContent(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := filepath.Join(targetDirectory, filepath.Base(file.Name))
|
||||
|
||||
var osFile *os.File
|
||||
|
||||
if overwriteIfExists {
|
||||
osFile, err = createFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
_, err = os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
osFile, err = createFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
return "", fmt.Errorf("a file already exists under the %s path. In order to overwrite it, set `overwriteIfExists` to true", path)
|
||||
}
|
||||
}
|
||||
defer osFile.Close()
|
||||
if _, err = io.Copy(osFile, response.Body); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (rs *restClient) retrieveDocumentContent(file *onepassword.File) (*http.Response, error) {
|
||||
span := rs.tracer.StartSpan("GetFileContent")
|
||||
defer span.Finish()
|
||||
|
||||
request, err := rs.buildRequest(http.MethodGet, file.ContentPath, http.NoBody, span)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := rs.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func createFile(path string) (*os.File, error) {
|
||||
osFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = os.Chmod(path, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return osFile, nil
|
||||
}
|
||||
|
||||
func (rs *restClient) buildRequest(method string, path string, body io.Reader, span opentracing.Span) (*http.Request, error) {
|
||||
url := fmt.Sprintf("%s%s", rs.URL, path)
|
||||
|
||||
@@ -394,3 +690,176 @@ func (rs *restClient) buildRequest(method string, path string, body io.Reader, s
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func loadToStruct(item *parsedItem, config reflect.Value) error {
|
||||
t := config.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
value := config.Field(i)
|
||||
field := t.Field(i)
|
||||
|
||||
if !value.CanSet() {
|
||||
return fmt.Errorf("cannot load config into private fields")
|
||||
}
|
||||
|
||||
item.fields = append(item.fields, &field)
|
||||
item.values = append(item.values, &value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadStructFromItem Load configuration values based on struct tag from one 1P item.
|
||||
// It accepts as parameters item title/UUID and vault title/UUID.
|
||||
func (rs *restClient) LoadStructFromItem(i interface{}, itemQuery string, vaultQuery string) error {
|
||||
if itemQuery == "" {
|
||||
return fmt.Errorf("Please provide either the item name or its ID.")
|
||||
}
|
||||
if isValidUUID(itemQuery) {
|
||||
return rs.LoadStructFromItemByUUID(i, itemQuery, vaultQuery)
|
||||
}
|
||||
return rs.LoadStructFromItemByTitle(i, itemQuery, vaultQuery)
|
||||
}
|
||||
|
||||
// LoadStructFromItemByUUID Load configuration values based on struct tag from one 1P item.
|
||||
func (rs *restClient) LoadStructFromItemByUUID(i interface{}, itemUUID string, vaultQuery string) error {
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isValidUUID(itemUUID) {
|
||||
return itemUUIDError
|
||||
}
|
||||
config, err := checkStruct(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item := parsedItem{}
|
||||
item.itemUUID = itemUUID
|
||||
item.vaultUUID = vaultUUID
|
||||
|
||||
if err := loadToStruct(&item, config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setValuesForTag(rs, &item, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadStructFromItemByTitle Load configuration values based on struct tag from one 1P item
|
||||
func (rs *restClient) LoadStructFromItemByTitle(i interface{}, itemTitle string, vaultQuery string) error {
|
||||
vaultUUID, err := rs.getVaultUUID(vaultQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := checkStruct(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item := parsedItem{}
|
||||
item.itemTitle = itemTitle
|
||||
item.vaultUUID = vaultUUID
|
||||
|
||||
if err := loadToStruct(&item, config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setValuesForTag(rs, &item, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadStruct Load configuration values based on struct tag
|
||||
func (rs *restClient) LoadStruct(i interface{}) error {
|
||||
config, err := checkStruct(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := config.Type()
|
||||
|
||||
// Multiple fields may be from a single item so we will collect them
|
||||
items := map[string]parsedItem{}
|
||||
|
||||
// Fetch the Vault from the environment
|
||||
vaultUUID, envVarFound := os.LookupEnv(envVaultVar)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
value := config.Field(i)
|
||||
field := t.Field(i)
|
||||
tag := field.Tag.Get(itemTag)
|
||||
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !value.CanSet() {
|
||||
return fmt.Errorf("Cannot load config into private fields")
|
||||
}
|
||||
|
||||
itemVault, err := vaultUUIDForField(&field, vaultUUID, envVarFound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isValidUUID(itemVault) {
|
||||
return vaultUUIDError
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s/%s", itemVault, tag)
|
||||
parsed := items[key]
|
||||
parsed.vaultUUID = itemVault
|
||||
parsed.itemTitle = tag
|
||||
parsed.fields = append(parsed.fields, &field)
|
||||
parsed.values = append(parsed.values, &value)
|
||||
items[key] = parsed
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if err := setValuesForTag(rs, &item, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseResponse(resp *http.Response, expectedStatusCode int, result interface{}) error {
|
||||
body, err := readResponseBody(resp, expectedStatusCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result != nil {
|
||||
if err := json.Unmarshal(body, result); err != nil {
|
||||
return fmt.Errorf("decoding response: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readResponseBody(resp *http.Response, expectedStatusCode int) ([]byte, error) {
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != expectedStatusCode {
|
||||
var errResp onepassword.Error
|
||||
if json.Valid(body) {
|
||||
if err := json.Unmarshal(body, &errResp); err != nil {
|
||||
return nil, fmt.Errorf("decoding error response: %s", err)
|
||||
}
|
||||
} else {
|
||||
errResp.StatusCode = resp.StatusCode
|
||||
errResp.Message = http.StatusText(resp.StatusCode)
|
||||
}
|
||||
return nil, &errResp
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func isValidUUID(u string) bool {
|
||||
r := regexp.MustCompile("^[a-z0-9]{26}$")
|
||||
return r.MatchString(u)
|
||||
}
|
||||
|
172
vendor/github.com/1Password/connect-sdk-go/connect/config.go
generated
vendored
172
vendor/github.com/1Password/connect-sdk-go/connect/config.go
generated
vendored
@@ -1,172 +0,0 @@
|
||||
package connect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
)
|
||||
|
||||
const (
|
||||
vaultTag = "opvault"
|
||||
itemTag = "opitem"
|
||||
fieldTag = "opfield"
|
||||
|
||||
envVaultVar = "OP_VAULT"
|
||||
)
|
||||
|
||||
type parsedItem struct {
|
||||
vaultUUID string
|
||||
itemTitle string
|
||||
fields []*reflect.StructField
|
||||
values []*reflect.Value
|
||||
}
|
||||
|
||||
// Load Load configuration values based on strcut tag
|
||||
func Load(client Client, i interface{}) error {
|
||||
configP := reflect.ValueOf(i)
|
||||
if configP.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("You must pass a pointer to Config struct")
|
||||
}
|
||||
|
||||
config := configP.Elem()
|
||||
if config.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("Config values can only be loaded into a struct")
|
||||
}
|
||||
|
||||
t := config.Type()
|
||||
|
||||
// Multiple fields may be from a single item so we will collect them
|
||||
items := map[string]parsedItem{}
|
||||
|
||||
// Fetch the Vault from the environment
|
||||
vaultUUID, envVarFound := os.LookupEnv(envVaultVar)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
value := config.Field(i)
|
||||
field := t.Field(i)
|
||||
tag := field.Tag.Get(itemTag)
|
||||
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !value.CanSet() {
|
||||
return fmt.Errorf("Cannot load config into private fields")
|
||||
}
|
||||
|
||||
itemVault, err := vaultUUIDForField(&field, vaultUUID, envVarFound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s/%s", itemVault, tag)
|
||||
parsed := items[key]
|
||||
parsed.vaultUUID = itemVault
|
||||
parsed.itemTitle = tag
|
||||
parsed.fields = append(parsed.fields, &field)
|
||||
parsed.values = append(parsed.values, &value)
|
||||
items[key] = parsed
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if err := setValuesForTag(client, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func vaultUUIDForField(field *reflect.StructField, vaultUUID string, envVaultFound bool) (string, error) {
|
||||
// Check to see if a specific vault has been specified on the field
|
||||
// If the env vault id has not been found and item doesn't have a vault
|
||||
// return an error
|
||||
if vaultUUIDTag := field.Tag.Get(vaultTag); vaultUUIDTag == "" {
|
||||
if !envVaultFound {
|
||||
return "", fmt.Errorf("There is no vault for %q field", field.Name)
|
||||
}
|
||||
} else {
|
||||
return vaultUUIDTag, nil
|
||||
}
|
||||
|
||||
return vaultUUID, nil
|
||||
}
|
||||
|
||||
func setValuesForTag(client Client, parsedItem *parsedItem) error {
|
||||
item, err := client.GetItemByTitle(parsedItem.itemTitle, parsedItem.vaultUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, field := range parsedItem.fields {
|
||||
value := parsedItem.values[i]
|
||||
path := field.Tag.Get(fieldTag)
|
||||
if path == "" {
|
||||
if field.Type == reflect.TypeOf(onepassword.Item{}) {
|
||||
value.Set(reflect.ValueOf(*item))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("There is no %q specified for %q", fieldTag, field.Name)
|
||||
}
|
||||
|
||||
pathParts := strings.Split(path, ".")
|
||||
|
||||
if len(pathParts) != 2 {
|
||||
return fmt.Errorf("Invalid field path format for %q", field.Name)
|
||||
}
|
||||
|
||||
sectionID := sectionIDForName(pathParts[0], item.Sections)
|
||||
label := pathParts[1]
|
||||
|
||||
for _, f := range item.Fields {
|
||||
fieldSectionID := ""
|
||||
if f.Section != nil {
|
||||
fieldSectionID = f.Section.ID
|
||||
}
|
||||
|
||||
if fieldSectionID == sectionID && f.Label == label {
|
||||
if err := setValue(value, f.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setValue(value *reflect.Value, toSet string) error {
|
||||
switch value.Kind() {
|
||||
case reflect.String:
|
||||
value.SetString(toSet)
|
||||
case reflect.Int:
|
||||
v, err := strconv.Atoi(toSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value.SetInt(int64(v))
|
||||
default:
|
||||
return fmt.Errorf("Unsupported type %q. Only string, int64, and onepassword.Item are supported", value.Kind())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sectionIDForName(name string, sections []*onepassword.ItemSection) string {
|
||||
if sections == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, s := range sections {
|
||||
if name == strings.ToLower(s.Label) {
|
||||
return s.ID
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
209
vendor/github.com/1Password/connect-sdk-go/connect/config_helper.go
generated
vendored
Normal file
209
vendor/github.com/1Password/connect-sdk-go/connect/config_helper.go
generated
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
package connect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
)
|
||||
|
||||
const (
|
||||
vaultTag = "opvault"
|
||||
itemTag = "opitem"
|
||||
sectionTag = "opsection"
|
||||
fieldTag = "opfield"
|
||||
urlTag = "opurl"
|
||||
|
||||
envVaultVar = "OP_VAULT"
|
||||
)
|
||||
|
||||
type parsedItem struct {
|
||||
vaultUUID string
|
||||
itemUUID string
|
||||
itemTitle string
|
||||
fields []*reflect.StructField
|
||||
values []*reflect.Value
|
||||
}
|
||||
|
||||
func checkStruct(i interface{}) (reflect.Value, error) {
|
||||
configP := reflect.ValueOf(i)
|
||||
if configP.Kind() != reflect.Ptr {
|
||||
return reflect.Value{}, fmt.Errorf("you must pass a pointer to Config struct")
|
||||
}
|
||||
|
||||
config := configP.Elem()
|
||||
if config.Kind() != reflect.Struct {
|
||||
return reflect.Value{}, fmt.Errorf("config values can only be loaded into a struct")
|
||||
}
|
||||
return config, nil
|
||||
|
||||
}
|
||||
func vaultUUIDForField(field *reflect.StructField, vaultUUID string, envVaultFound bool) (string, error) {
|
||||
// Check to see if a specific vault has been specified on the field
|
||||
// If the env vault id has not been found and item doesn't have a vault
|
||||
// return an error
|
||||
if vaultUUIDTag := field.Tag.Get(vaultTag); vaultUUIDTag == "" {
|
||||
if !envVaultFound {
|
||||
return "", fmt.Errorf("There is no vault for %q field", field.Name)
|
||||
}
|
||||
} else {
|
||||
return vaultUUIDTag, nil
|
||||
}
|
||||
|
||||
return vaultUUID, nil
|
||||
}
|
||||
|
||||
func setValuesForTag(client Client, parsedItem *parsedItem, byTitle bool) error {
|
||||
var item *onepassword.Item
|
||||
var err error
|
||||
if byTitle {
|
||||
item, err = client.GetItemByTitle(parsedItem.itemTitle, parsedItem.vaultUUID)
|
||||
} else {
|
||||
item, err = client.GetItem(parsedItem.itemUUID, parsedItem.vaultUUID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, field := range parsedItem.fields {
|
||||
value := parsedItem.values[i]
|
||||
|
||||
if field.Type == reflect.TypeOf(onepassword.ItemURL{}) {
|
||||
url := &onepassword.ItemURL{
|
||||
Primary: urlPrimaryForName(field.Tag.Get(urlTag), item.URLs),
|
||||
Label: urlLabelForName(field.Tag.Get(urlTag), item.URLs),
|
||||
URL: urlURLForName(field.Tag.Get(urlTag), item.URLs),
|
||||
}
|
||||
value.Set(reflect.ValueOf(*url))
|
||||
continue
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s.%s", field.Tag.Get(sectionTag), field.Tag.Get(fieldTag))
|
||||
if path == "." {
|
||||
if field.Type == reflect.TypeOf(onepassword.Item{}) {
|
||||
value.Set(reflect.ValueOf(*item))
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("There is no %q specified for %q", fieldTag, field.Name)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, ".") {
|
||||
if field.Type == reflect.TypeOf(onepassword.ItemSection{}) {
|
||||
section := &onepassword.ItemSection{
|
||||
ID: sectionIDForName(field.Tag.Get(sectionTag), item.Sections),
|
||||
Label: sectionLabelForName(field.Tag.Get(sectionTag), item.Sections),
|
||||
}
|
||||
value.Set(reflect.ValueOf(*section))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sectionID := sectionIDForName(field.Tag.Get(sectionTag), item.Sections)
|
||||
|
||||
for _, f := range item.Fields {
|
||||
fieldSectionID := ""
|
||||
if f.Section != nil {
|
||||
fieldSectionID = f.Section.ID
|
||||
}
|
||||
|
||||
if fieldSectionID == sectionID && f.Label == field.Tag.Get(fieldTag) {
|
||||
if err := setValue(value, f.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setValue(value *reflect.Value, toSet string) error {
|
||||
switch value.Kind() {
|
||||
case reflect.String:
|
||||
value.SetString(toSet)
|
||||
case reflect.Int:
|
||||
v, err := strconv.Atoi(toSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value.SetInt(int64(v))
|
||||
default:
|
||||
return fmt.Errorf("Unsupported type %q. Only string, int64, and onepassword.Item are supported", value.Kind())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sectionIDForName(name string, sections []*onepassword.ItemSection) string {
|
||||
if sections == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, s := range sections {
|
||||
if name == strings.ToLower(s.Label) {
|
||||
return s.ID
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func sectionLabelForName(name string, sections []*onepassword.ItemSection) string {
|
||||
if sections == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, s := range sections {
|
||||
if name == strings.ToLower(s.Label) {
|
||||
return s.Label
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func urlPrimaryForName(name string, itemURLs []onepassword.ItemURL) bool {
|
||||
if itemURLs == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, url := range itemURLs {
|
||||
if url.Label == strings.ToLower(name) {
|
||||
return url.Primary
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func urlLabelForName(name string, itemURLs []onepassword.ItemURL) string {
|
||||
if itemURLs == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, url := range itemURLs {
|
||||
if url.Label == strings.ToLower(name) {
|
||||
return url.Label
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func urlURLForName(name string, itemURLs []onepassword.ItemURL) string {
|
||||
if itemURLs == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, url := range itemURLs {
|
||||
if url.Label == strings.ToLower(name) {
|
||||
return url.URL
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
|
||||
}
|
101
vendor/github.com/1Password/connect-sdk-go/connect/version.go
generated
vendored
101
vendor/github.com/1Password/connect-sdk-go/connect/version.go
generated
vendored
@@ -1,5 +1,104 @@
|
||||
package connect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SDKVersion is the latest Semantic Version of the library
|
||||
// Do not rename this variable without changing the regex in the Makefile
|
||||
const SDKVersion = "1.0.1"
|
||||
const SDKVersion = "1.5.1"
|
||||
|
||||
const VersionHeaderKey = "1Password-Connect-Version"
|
||||
|
||||
// expectMinimumConnectVersion returns an error if the provided minimum version for Connect is lower than the version
|
||||
// reported in the response from Connect.
|
||||
func expectMinimumConnectVersion(resp *http.Response, minimumVersion version) error {
|
||||
serverVersion, err := getServerVersion(resp)
|
||||
if err != nil {
|
||||
// Return gracefully if server version cannot be determined reliably
|
||||
return nil
|
||||
}
|
||||
if !serverVersion.IsGreaterOrEqualThan(minimumVersion) {
|
||||
return fmt.Errorf("need at least version %s of Connect for this function, detected version %s. Please update your Connect server", minimumVersion, serverVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServerVersion(resp *http.Response) (serverVersion, error) {
|
||||
versionHeader := resp.Header.Get(VersionHeaderKey)
|
||||
if versionHeader == "" {
|
||||
// The last version without the version header was v1.2.0
|
||||
return serverVersion{
|
||||
version: version{1, 2, 0},
|
||||
orEarlier: true,
|
||||
}, nil
|
||||
}
|
||||
return parseServerVersion(versionHeader)
|
||||
}
|
||||
|
||||
type version struct {
|
||||
major int
|
||||
minor int
|
||||
patch int
|
||||
}
|
||||
|
||||
// serverVersion describes the version reported by the server.
|
||||
type serverVersion struct {
|
||||
version
|
||||
// orEarlier is true if the version is derived from the lack of a version header from the server.
|
||||
orEarlier bool
|
||||
}
|
||||
|
||||
func (v version) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch)
|
||||
}
|
||||
|
||||
func (v serverVersion) String() string {
|
||||
if v.orEarlier {
|
||||
return v.version.String() + " (or earlier)"
|
||||
}
|
||||
return v.version.String()
|
||||
}
|
||||
|
||||
// IsGreaterOrEqualThan returns true if the lefthand-side version is equal to or or a higher version than the provided
|
||||
// minimum according to the semantic versioning rules.
|
||||
func (v version) IsGreaterOrEqualThan(min version) bool {
|
||||
if v.major != min.major {
|
||||
// Different major version
|
||||
return v.major > min.major
|
||||
}
|
||||
|
||||
if v.minor != min.minor {
|
||||
// Same major, but different minor version
|
||||
return v.minor > min.minor
|
||||
}
|
||||
|
||||
// Same major and minor version
|
||||
return v.patch >= min.patch
|
||||
}
|
||||
|
||||
func parseServerVersion(v string) (serverVersion, error) {
|
||||
spl := strings.Split(v, ".")
|
||||
if len(spl) != 3 {
|
||||
return serverVersion{}, errors.New("wrong length")
|
||||
}
|
||||
var res [3]int
|
||||
for i := range res {
|
||||
tmp, err := strconv.Atoi(spl[i])
|
||||
if err != nil {
|
||||
return serverVersion{}, err
|
||||
}
|
||||
res[i] = tmp
|
||||
}
|
||||
return serverVersion{
|
||||
version: version{
|
||||
major: res[0],
|
||||
minor: res[1],
|
||||
patch: res[2],
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
21
vendor/github.com/1Password/connect-sdk-go/onepassword/errors.go
generated
vendored
Normal file
21
vendor/github.com/1Password/connect-sdk-go/onepassword/errors.go
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package onepassword
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Error is an error returned by the Connect API.
|
||||
type Error struct {
|
||||
StatusCode int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("status %d: %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
func (e *Error) Is(target error) bool {
|
||||
t, ok := target.(*Error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return t.Message == e.Message && t.StatusCode == e.StatusCode
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user