Merge pull request #124 from 1Password/feature/migrate-operator

Migrate operator to the latest operator-sdk
This commit is contained in:
Eduard Filip
2022-11-10 19:07:30 +01:00
committed by GitHub
3427 changed files with 611912 additions and 107049 deletions

4
.dockerignore Normal file
View File

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

View File

@@ -7,15 +7,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: ^1.15 go-version: ^1.19
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Build - name: Build
run: go build -v ./... run: go build -v ./...
- name: Test - name: Test
run: go test -v ./... -cover run: make test

View File

@@ -14,9 +14,10 @@ jobs:
outputs: outputs:
result: ${{ steps.is_release_branch_without_pr.outputs.result }} result: ${{ steps.is_release_branch_without_pr.outputs.result }}
steps: steps:
- id: is_release_branch_without_pr -
id: is_release_branch_without_pr
name: Find matching PR name: Find matching PR
uses: actions/github-script@v3 uses: actions/github-script@v6
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
@@ -27,7 +28,7 @@ jobs:
if(!releaseBranchName) { return false } if(!releaseBranchName) { return false }
const {data: prs} = await github.pulls.list({ const {data: prs} = await github.rest.pulls.list({
...context.repo, ...context.repo,
state: 'open', state: 'open',
head: `1Password:${releaseBranchName}`, head: `1Password:${releaseBranchName}`,
@@ -42,7 +43,7 @@ jobs:
name: Create Release Pull Request name: Create Release Pull Request
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Parse release version - name: Parse release version
id: get_version id: get_version

View File

@@ -11,15 +11,14 @@ jobs:
env: env:
DOCKER_CLI_EXPERIMENTAL: "enabled" DOCKER_CLI_EXPERIMENTAL: "enabled"
steps: steps:
- - name: Checkout
name: Checkout uses: actions/checkout@v3
uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
-
name: Docker meta - name: Docker meta
id: meta id: meta
uses: crazy-max/ghaction-docker-meta@v2 uses: docker/metadata-action@v4
with: with:
images: | images: |
1password/onepassword-operator 1password/onepassword-operator
@@ -28,24 +27,25 @@ jobs:
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
- name: Get the version from tag - name: Get the version from tag
id: get_version id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
-
name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
-
name: Docker Login - name: Docker Login
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
file: Dockerfile file: Dockerfile

87
.gitignore vendored
View File

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

View File

@@ -1,20 +1,31 @@
# Build the manager binary # Build the manager binary
FROM golang:1.13 as builder FROM golang:1.19 as builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace WORKDIR /workspace
# Copy the Go Modules manifests # Copy the Go Modules manifests
COPY go.mod go.mod COPY go.mod go.mod
COPY go.sum go.sum 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 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 pkg/ pkg/
COPY version/ version/ COPY version/ version/
COPY vendor/ vendor/ COPY vendor/ vendor/
# Build # 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 \ RUN CGO_ENABLED=0 \
GO111MODULE=on \ GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build \ go build \
-ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \ -ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \
-mod vendor \ -mod vendor \
@@ -25,7 +36,7 @@ RUN CGO_ENABLED=0 \
FROM gcr.io/distroless/static:nonroot FROM gcr.io/distroless/static:nonroot
WORKDIR / WORKDIR /
COPY --from=builder /workspace/manager . COPY --from=builder /workspace/manager .
USER nonroot:nonroot USER 65532:65532
COPY deploy/connect/ deploy/connect/ COPY config/connect/ config/connect/
ENTRYPOINT ["/manager"] ENTRYPOINT ["/manager"]

275
Makefile
View File

@@ -1,43 +1,262 @@
export MAIN_BRANCH ?= main # VERSION defines the project version for the bundle.
# Update this value when you upgrade the version of your project.
# To re-generate a bundle for another specific version without changing the standard setup, you can:
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
VERSION ?= 1.5.0
.DEFAULT_GOAL := help # CHANNELS define the bundle channels used in the bundle.
.PHONY: test build build/binary build/local clean test/coverage release/prepare release/tag .check_bump_type .check_git_clean help # 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
GIT_BRANCH := $(shell git symbolic-ref --short HEAD) # DEFAULT_CHANNEL defines the default channel used in the bundle.
WORKTREE_CLEAN := $(shell git status --porcelain 1>/dev/null 2>&1; echo $$?) # Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable")
SCRIPTS_DIR := $(CURDIR)/scripts # 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)
versionFile = $(CURDIR)/.VERSION # IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images.
curVersion := $(shell cat $(versionFile) | sed 's/^v//') # 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
OPERATOR_NAME := onepassword-connect-operator # BUNDLE_IMG defines the image:tag used for the bundle.
DOCKER_IMG_TAG ?= $(OPERATOR_NAME):v$(curVersion) # 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)
test: ## Run test suite # BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command
go test ./... BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
test/coverage: ## Run test suite with coverage report # USE_IMAGE_DIGESTS defines if images are resolved via tags or digests
go test -v ./... -cover # 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
build: ## Build operator Docker image # Image URL to use all building/pushing image targets
@docker build -f Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG) . IMG ?= 1password/onepassword-operator:latest
@echo "Successfully built and tagged image." # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
@echo "Tag: $(DOCKER_IMG_TAG)" ENVTEST_K8S_VERSION = 1.24.2
build/local: ## Build local version of the operator Docker image # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
@docker build -f Dockerfile -t local/$(DOCKER_IMG_TAG) . ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
build/binary: clean ## Build operator binary # Setting SHELL to bash allows bash commands to be executed by recipes.
@mkdir -p dist # Options are set to exit when a recipe line exits non-zero or a piped command fails.
@go build -mod vendor -a -o manager ./cmd/manager/main.go SHELL = /usr/bin/env bash -o pipefail
@mv manager ./dist .SHELLFLAGS = -ec
clean: .PHONY: all
rm -rf ./dist all: build
help: ## Prints this help message ##@ General
@grep -E '^[\/a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
# 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: 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 ===================== ## Release functions =====================

18
PROJECT Normal file
View 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"

162
README.md
View File

@@ -6,26 +6,39 @@ 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. 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/) - [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/)
- [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/) - [`kubectl` installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [docker installed](https://docs.docker.com/get-docker/) - [`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/) - [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)
- [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. - [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 #### 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) [The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts)
#### Deploy using the Connect Operator #### 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.
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: Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session:
@@ -35,16 +48,20 @@ cat 1password-credentials.json | base64 | \
``` ```
Create a Kubernetes secret from the op-session file: Create a Kubernetes secret from the op-session file:
```bash ```bash
kubectl create secret generic op-credentials --from-file=op-session 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 ```yaml
- name: MANAGE_CONNECT - name: MANAGE_CONNECT
value: "true" 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 ### Kubernetes Operator Deployment
**Create Kubernetes Secret for OP_CONNECT_TOKEN** **Create Kubernetes Secret for OP_CONNECT_TOKEN**
@@ -56,43 +73,33 @@ kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT
``` ```
If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command: 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 ```bash
kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>) kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
``` ```
[More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens)
**Set Permissions For Operator**
We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder.
```bash
kubectl apply -f deploy/permissions.yaml
```
**Create Custom One Password Secret Resource**
```bash
kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
```
**Deploying the Operator** **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. - **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. - **WATCH_NAMESPACE:** (default: watch all namespaces): Comma separated list of what Namespaces to watch for changes.
- **POLLING_INTERVAL** (default: 600): The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. - **POLLING_INTERVAL** (default: 600): The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password 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. - **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. - **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 ```shell
kubectl apply -f deploy/operator.yaml make deploy
```
**Undeploy Operator**
```
make undeploy
``` ```
## Usage ## Usage
@@ -146,26 +153,29 @@ Note: Deleting the Deployment that you've created will automatically delete the
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes. If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
--- ---
**NOTE** **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: 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 `-` - Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed
- All the letters will be lower-cased. - 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. 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: at the operator level, per-namespace, or per-deployment.
**On the operator**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environemnt variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false. **On the operator**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environemnt variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false.
**Per Namespace**: This method allows for managing auto restarts on all deployments within a namespace. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired namespace. An example of this is shown below: **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 ```yaml
# enabled auto restarts for all deployments within a namespace unless overwritten within a deployment # enabled auto restarts for all deployments within a namespace unless overwritten within a deployment
apiVersion: v1 apiVersion: v1
@@ -175,10 +185,12 @@ metadata:
annotations: annotations:
operator.1password.io/auto-restart: "true" operator.1password.io/auto-restart: "true"
``` ```
If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment. 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** **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: 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 ```yaml
# enabled auto restarts for the deployment # enabled auto restarts for the deployment
apiVersion: v1 apiVersion: v1
@@ -188,10 +200,12 @@ metadata:
annotations: annotations:
operator.1password.io/auto-restart: "true" operator.1password.io/auto-restart: "true"
``` ```
If the value is not set, the auto restart 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** **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: 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 ```yaml
# enabled auto restarts for the OnePasswordItem # enabled auto restarts for the OnePasswordItem
apiVersion: onepassword.com/v1 apiVersion: onepassword.com/v1
@@ -201,34 +215,78 @@ metadata:
annotations: annotations:
operator.1password.io/auto-restart: "true" operator.1password.io/auto-restart: "true"
``` ```
If the value is not set, the auto restart settings on the deployment will be used. If the value is not set, the auto restart settings on the deployment will be used.
<!--
## Getting Started
Youll 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 ## Development
### Creating a Docker image ### How it works
To create a local version of the Docker image for testing, use the following `Makefile` target: This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
```shell
make build/local It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/)
which provides a reconcile function responsible for synchronizing resources untile 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 2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running):
```shell
make build/binary ```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 If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
make test
```sh
make manifests
``` ```
With coverage: **NOTE:** Run `make --help` for more information on all potential `make` targets
```shell
make test/coverage More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
```
## Security ## Security

View 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
)

View File

@@ -1,13 +1,41 @@
/*
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 package v1
import ( import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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. // 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 // OnePasswordItemSpec defines the desired state of OnePasswordItem
type OnePasswordItemSpec struct { 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"` ItemPath string `json:"itemPath,omitempty"`
} }
@@ -33,28 +61,27 @@ type OnePasswordItemCondition struct {
// OnePasswordItemStatus defines the observed state of OnePasswordItem // OnePasswordItemStatus defines the observed state of OnePasswordItem
type OnePasswordItemStatus struct { type OnePasswordItemStatus struct {
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html // Important: Run "make" to regenerate code after modifying this file
Conditions []OnePasswordItemCondition `json:"conditions"` Conditions []OnePasswordItemCondition `json:"conditions"`
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object //+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// OnePasswordItem is the Schema for the onepassworditems API // OnePasswordItem is the Schema for the onepassworditems API
// +kubebuilder:subresource:status
// +kubebuilder:resource:path=onepassworditems,scope=Namespaced
type OnePasswordItem struct { type OnePasswordItem struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"` metav1.ObjectMeta `json:"metadata,omitempty"`
// Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types // Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Spec OnePasswordItemSpec `json:"spec,omitempty"` Spec OnePasswordItemSpec `json:"spec,omitempty"`
Status OnePasswordItemStatus `json:"status,omitempty"` Status OnePasswordItemStatus `json:"status,omitempty"`
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object //+kubebuilder:object:root=true
// OnePasswordItemList contains a list of OnePasswordItem // OnePasswordItemList contains a list of OnePasswordItem
type OnePasswordItemList struct { type OnePasswordItemList struct {

View File

@@ -1,7 +1,31 @@
//go:build !ignore_autogenerated //go:build !ignore_autogenerated
// +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 package v1
@@ -16,7 +40,6 @@ func (in *OnePasswordItem) DeepCopyInto(out *OnePasswordItem) {
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status) in.Status.DeepCopyInto(&out.Status)
return
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem.
@@ -41,7 +64,6 @@ func (in *OnePasswordItem) DeepCopyObject() runtime.Object {
func (in *OnePasswordItemCondition) DeepCopyInto(out *OnePasswordItemCondition) { func (in *OnePasswordItemCondition) DeepCopyInto(out *OnePasswordItemCondition) {
*out = *in *out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
return
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemCondition. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemCondition.
@@ -66,7 +88,6 @@ func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
} }
return
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemList. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemList.
@@ -90,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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OnePasswordItemSpec) DeepCopyInto(out *OnePasswordItemSpec) { func (in *OnePasswordItemSpec) DeepCopyInto(out *OnePasswordItemSpec) {
*out = *in *out = *in
return
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemSpec. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemSpec.
@@ -113,7 +133,6 @@ func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
} }
return
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus.

View File

@@ -1,15 +0,0 @@
FROM registry.access.redhat.com/ubi8/ubi-minimal:latest
ENV OPERATOR=/usr/local/bin/onepassword-connect-operator \
USER_UID=1001 \
USER_NAME=onepassword-connect-operator
# install operator binary
COPY build/_output/bin/op-kubernetes-connect-operator ${OPERATOR}
COPY build/bin /usr/local/bin
RUN /usr/local/bin/user_setup
ENTRYPOINT ["/usr/local/bin/entrypoint"]
USER ${USER_UID}

View File

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

View File

@@ -1,11 +0,0 @@
#!/bin/sh
set -x
# ensure $HOME exists and is accessible by group 0 (we don't know what the runtime UID will be)
echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd
mkdir -p "${HOME}"
chown "${USER_UID}:0" "${HOME}"
chmod ug+rwx "${HOME}"
# no need for this script to remain in the image after running
rm "$0"

View File

@@ -1,305 +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:
err := updatedSecretsPoller.UpdateKubernetesSecretsTask()
if err != nil {
log.Error(err, "error running update kubernetes secret task")
}
}
}
}()
// Start the Cmd
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
log.Error(err, "Manager exited non-zero")
done <- true
os.Exit(1)
}
}
// addMetrics will create the Services and Service Monitors to allow the operator export the metrics by using
// the Prometheus operator
func addMetrics(ctx context.Context, cfg *rest.Config) {
// Get the namespace the operator is currently deployed in.
operatorNs, err := k8sutil.GetOperatorNamespace()
if err != nil {
if errors.Is(err, k8sutil.ErrRunLocal) {
log.Info("Skipping CR metrics server creation; not running in a cluster.")
return
}
}
if err := serveCRMetrics(cfg, operatorNs); err != nil {
log.Info("Could not generate and serve custom resource metrics", "error", err.Error())
}
// Add to the below struct any other metrics ports you want to expose.
servicePorts := []v1.ServicePort{
{Port: metricsPort, Name: metrics.OperatorPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: metricsPort}},
{Port: operatorMetricsPort, Name: metrics.CRPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: operatorMetricsPort}},
}
// Create Service object to expose the metrics port(s).
service, err := metrics.CreateMetricsService(ctx, cfg, servicePorts)
if err != nil {
log.Info("Could not create metrics Service", "error", err.Error())
}
// CreateServiceMonitors will automatically create the prometheus-operator ServiceMonitor resources
// necessary to configure Prometheus to scrape metrics from this operator.
services := []*v1.Service{service}
// The ServiceMonitor is created in the same namespace where the operator is deployed
_, err = metrics.CreateServiceMonitors(cfg, operatorNs, services)
if err != nil {
log.Info("Could not create ServiceMonitor object", "error", err.Error())
// If this operator is deployed to a cluster without the prometheus-operator running, it will return
// ErrServiceMonitorNotPresent, which can be used to safely skip ServiceMonitor creation.
if err == metrics.ErrServiceMonitorNotPresent {
log.Info("Install prometheus-operator in your cluster to create ServiceMonitor objects", "error", err.Error())
}
}
}
// serveCRMetrics gets the Operator/CustomResource GVKs and generates metrics based on those types.
// It serves those metrics on "http://metricsHost:operatorMetricsPort".
func serveCRMetrics(cfg *rest.Config, operatorNs string) error {
// The function below returns a list of filtered operator/CR specific GVKs. For more control, override the GVK list below
// with your own custom logic. Note that if you are adding third party API schemas, probably you will need to
// customize this implementation to avoid permissions issues.
filteredGVK, err := k8sutil.GetGVKsFromAddToScheme(apis.AddToScheme)
if err != nil {
return err
}
// The metrics will be generated from the namespaces which are returned here.
// NOTE that passing nil or an empty list of namespaces in GenerateAndServeCRMetrics will result in an error.
ns, err := kubemetrics.GetNamespacesForMetrics(operatorNs)
if err != nil {
return err
}
// Generate and serve custom resource specific metrics.
err = kubemetrics.GenerateAndServeCRMetrics(cfg, ns, filteredGVK, metricsHost, operatorMetricsPort)
if err != nil {
return err
}
return nil
}
func getPollingIntervalForUpdatingSecrets() time.Duration {
timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable)
if found {
timeInSeconds, err := strconv.Atoi(timeInSecondsString)
if err == nil {
return time.Duration(timeInSeconds) * time.Second
}
log.Info("Invalid value set for polling interval. Must be a valid integer.")
}
log.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval))
return time.Duration(defaultPollingInterval) * time.Second
}
func shouldManageConnect() bool {
shouldManageConnect, found := os.LookupEnv(manageConnect)
if found {
shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect))
if err != nil {
log.Error(err, "")
os.Exit(1)
}
return shouldManageConnectBool
}
return false
}
func shouldAutoRestartDeployments() bool {
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
if found {
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
if err != nil {
log.Error(err, "")
os.Exit(1)
}
return shouldAutoRestartDeploymentsBool
}
return false
}

View File

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

View File

@@ -1,6 +1,10 @@
---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.9.2
creationTimestamp: null
name: onepassworditems.onepassword.com name: onepassworditems.onepassword.com
spec: spec:
group: onepassword.com group: onepassword.com
@@ -38,9 +42,6 @@ spec:
description: OnePasswordItemStatus defines the observed state of OnePasswordItem description: OnePasswordItemStatus defines the observed state of OnePasswordItem
properties: properties:
conditions: conditions:
description: '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'
items: items:
properties: properties:
lastTransitionTime: lastTransitionTime:

View 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

View File

@@ -0,0 +1,19 @@
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/name
namespace:
- kind: CustomResourceDefinition
version: v1
group: apiextensions.k8s.io
path: spec/conversion/webhook/clientConfig/service/namespace
create: false
varReference:
- path: metadata/annotations

View File

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

View 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

View 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

View File

@@ -0,0 +1,39 @@
# 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:
containers:
- name: kube-rbac-proxy
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0
args:
- "--secure-listen-address=0.0.0.0:8443"
- "--upstream=http://127.0.0.1:8080/"
- "--logtostderr=true"
- "--v=0"
ports:
- containerPort: 8443
protocol: TCP
name: https
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 5m
memory: 64Mi
- name: manager
args:
- "--health-probe-bind-address=:8081"
- "--metrics-bind-address=127.0.0.1:8080"
- "--leader-elect"

View File

@@ -0,0 +1,20 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: onepassword-connect-operator
namespace: system
spec:
template:
spec:
containers:
- name: manager
args:
- "--config=controller_manager_config.yaml"
volumeMounts:
- name: manager-config
mountPath: /controller_manager_config.yaml
subPath: controller_manager_config.yaml
volumes:
- name: manager-config
configMap:
name: manager-config

View 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

View File

@@ -0,0 +1,10 @@
resources:
- manager.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: manager-config
files:
- controller_manager_config.yaml

View 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

View File

@@ -0,0 +1,28 @@
# These resources constitute the fully configured set of manifests
# used to generate the 'manifests/' directory in a bundle.
resources:
- bases/onepassword-operator.clusterserviceversion.yaml
- ../default
- ../samples
- ../scorecard
# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix.
# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager.
# These patches remove the unnecessary "cert" volume and its manager container volumeMount.
#patchesJson6902:
#- target:
# group: apps
# version: v1
# kind: Deployment
# name: controller-manager
# namespace: system
# patch: |-
# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs.
# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment.
# - op: remove
# path: /spec/template/spec/containers/0/volumeMounts/0
# # Remove the "cert" volume, since OLM will create and mount a set of certs.
# # Update the indices in this path if adding or removing volumes in the manager's Deployment.
# - op: remove
# path: /spec/template/spec/volumes/0

View File

@@ -0,0 +1,2 @@
resources:
- monitor.yaml

View 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

View File

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

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole
metadata: metadata:
creationTimestamp: null creationTimestamp: null
name: onepassword-connect-operator name: manager-role
rules: rules:
- apiGroups: - apiGroups:
- "" - ""
resources: resources:
- configmaps
- endpoints
- events
- namespaces
- persistentvolumeclaims
- pods - pods
- secrets
- services - services
- services/finalizers - services/finalizers
- endpoints
- persistentvolumeclaims
- events
- configmaps
- secrets
- namespaces
verbs: verbs:
- create - create
- delete - delete
@@ -43,11 +25,17 @@ rules:
- patch - patch
- update - update
- watch - watch
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups: - apiGroups:
- apps - apps
resources: resources:
- deployments
- daemonsets - daemonsets
- deployments
- replicasets - replicasets
- statefulsets - statefulsets
verbs: verbs:
@@ -59,12 +47,30 @@ rules:
- update - update
- watch - watch
- apiGroups: - apiGroups:
- monitoring.coreos.com - apps
resources: resources:
- servicemonitors - deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
- deployments
- replicasets
verbs: verbs:
- get - get
- create - apiGroups:
- apps
resources:
- deployments/finalizers
verbs:
- update
- apiGroups: - apiGroups:
- apps - apps
resourceNames: resourceNames:
@@ -73,19 +79,21 @@ rules:
- deployments/finalizers - deployments/finalizers
verbs: verbs:
- update - update
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups: - apiGroups:
- apps - apps
resources: resources:
- replicasets - deployments/status
- deployments
verbs: verbs:
- get - get
- patch
- update
- apiGroups:
- monitoring.coreos.com
resources:
- servicemonitors
verbs:
- create
- get
- apiGroups: - apiGroups:
- onepassword.com - onepassword.com
resources: resources:
@@ -98,3 +106,29 @@ rules:
- patch - patch
- update - 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

View 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

View File

@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: onepassword-connect-operator
namespace: system

View File

@@ -0,0 +1,4 @@
## Append samples you want in your CSV to this file as resources ##
resources:
- onepassword_v1_onepassworditem.yaml
#+kubebuilder:scaffold:manifestskustomizesamples

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
resources:
- bases/config.yaml
patchesJson6902:
- path: patches/basic.config.yaml
target:
group: scorecard.operatorframework.io
version: v1alpha3
kind: Configuration
name: config
- path: patches/olm.config.yaml
target:
group: scorecard.operatorframework.io
version: v1alpha3
kind: Configuration
name: config
#+kubebuilder:scaffold:patchesJson6902

View File

@@ -0,0 +1,10 @@
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- basic-check-spec
image: quay.io/operator-framework/scorecard-test:v1.23.0
labels:
suite: basic
test: basic-check-spec-test

View File

@@ -0,0 +1,50 @@
- op: add
path: /stages/0/tests/-
value:
entrypoint:
- scorecard-test
- olm-bundle-validation
image: quay.io/operator-framework/scorecard-test:v1.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

View File

@@ -0,0 +1,213 @@
/*
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]
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 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)
}

View 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())
})
})
})

View 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"
"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
r.Delete(context.Background(), kubernetesSecret)
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,
}
}

View 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
View 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())
})

View File

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

View File

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

View File

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

94
go.mod
View File

@@ -1,21 +1,87 @@
module github.com/1Password/onepassword-operator module github.com/1Password/onepassword-operator
go 1.13 go 1.18
require ( require (
github.com/1Password/connect-sdk-go v1.2.0 github.com/1Password/connect-sdk-go v1.5.0
github.com/operator-framework/operator-sdk v0.19.0 github.com/onsi/ginkgo/v2 v2.1.6
github.com/prometheus/common v0.14.0 // indirect github.com/onsi/gomega v1.20.2
github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.0
github.com/stretchr/testify v1.7.0 k8s.io/api v0.25.3
k8s.io/api v0.18.2 k8s.io/apimachinery v0.25.3
k8s.io/apimachinery v0.18.2 k8s.io/client-go v0.25.3
k8s.io/client-go v12.0.0+incompatible k8s.io/kubectl v0.25.0
k8s.io/kubectl v0.18.2 sigs.k8s.io/controller-runtime v0.13.0
sigs.k8s.io/controller-runtime v0.6.0
) )
replace ( require (
github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM cloud.google.com/go/compute v1.10.0 // indirect
k8s.io/client-go => k8s.io/client-go v0.18.2 // Required by prometheus-operator github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/zapr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // 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/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.13 // 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.2 // 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.13.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.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.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/oauth2 v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.1.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.25.3 // indirect
k8s.io/component-base v0.25.3 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20221012122500-cfd413dd9e85 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
) )

1504
go.sum

File diff suppressed because it is too large Load Diff

23
hack/boilerplate.go.txt Normal file
View 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
View 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
}

View File

@@ -1,10 +0,0 @@
package apis
import (
v1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
)
func init() {
// Register the types with the Scheme so the components can map objects to GroupVersionKinds and back
AddToSchemes = append(AddToSchemes, v1.SchemeBuilder.AddToScheme)
}

View File

@@ -1,13 +0,0 @@
package apis
import (
"k8s.io/apimachinery/pkg/runtime"
)
// AddToSchemes may be used to add all resources defined in the project to a Scheme
var AddToSchemes runtime.SchemeBuilder
// AddToScheme adds all Resources to the Scheme
func AddToScheme(s *runtime.Scheme) error {
return AddToSchemes.AddToScheme(s)
}

View File

@@ -1,6 +0,0 @@
// Package onepassword contains onepassword API versions.
//
// This file ensures Go source parsers acknowledge the onepassword package
// and any child packages. It can be removed if any other Go source files are
// added to this package.
package onepassword

View File

@@ -1,4 +0,0 @@
// Package v1 contains API Schema definitions for the onepassword v1 API group
// +k8s:deepcopy-gen=package,register
// +groupName=onepassword.com
package v1

View File

@@ -1,19 +0,0 @@
// NOTE: Boilerplate only. Ignore this file.
// Package v1 contains API Schema definitions for the onepassword v1 API group
// +k8s:deepcopy-gen=package,register
// +groupName=onepassword.com
package v1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: "onepassword.com", Version: "v1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
)

View File

@@ -1,10 +0,0 @@
package controller
import (
"github.com/1Password/onepassword-operator/pkg/controller/deployment"
)
func init() {
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs, deployment.Add)
}

View File

@@ -1,10 +0,0 @@
package controller
import (
"github.com/1Password/onepassword-operator/pkg/controller/onepassworditem"
)
func init() {
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs, onepassworditem.Add)
}

View File

@@ -1,19 +0,0 @@
package controller
import (
"github.com/1Password/connect-sdk-go/connect"
"sigs.k8s.io/controller-runtime/pkg/manager"
)
// AddToManagerFuncs is a list of functions to add all Controllers to the Manager
var AddToManagerFuncs []func(manager.Manager, connect.Client) error
// AddToManager adds all Controllers to the Manager
func AddToManager(m manager.Manager, opConnectClient connect.Client) error {
for _, f := range AddToManagerFuncs {
if err := f(m, opConnectClient); err != nil {
return err
}
}
return nil
}

View File

@@ -1,222 +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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
var 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, 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(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error {
reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
secretName := annotations[op.NameAnnotation]
secretLabels := map[string]string(nil)
secretType := ""
if len(secretName) == 0 {
reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.")
return nil
}
item, err := op.GetOnePasswordItemByPath(r.opConnectClient, annotations[op.ItemPathAnnotation])
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}
// Create owner reference.
gvk, err := apiutil.GVKForObject(deployment, r.scheme)
if err != nil {
return fmt.Errorf("could not to retrieve group version kind: %v", err)
}
ownerRef := &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: deployment.GetName(),
UID: deployment.GetUID(),
}
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef)
}

View File

@@ -1,481 +0,0 @@
package deployment
import (
"context"
"fmt"
"regexp"
"testing"
"github.com/1Password/onepassword-operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
errors2 "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubectl/pkg/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const (
deploymentKind = "Deployment"
deploymentAPIVersion = "v1"
name = "test-deployment"
namespace = "default"
vaultId = "hfnjvi6aymbsnfc2xeeoheizda"
itemId = "nwrhuano7bcwddcviubpp4mhfq"
username = "test-user"
password = "QmHumKc$mUeEem7caHtbaBaJ"
userKey = "username"
passKey = "password"
version = 123
)
type testReconcileItem struct {
testName string
deploymentResource *appsv1.Deployment
existingSecret *corev1.Secret
expectedError error
expectedResultSecret *corev1.Secret
expectedEvents []string
opItem map[string]string
existingDeployment *appsv1.Deployment
}
var (
expectedSecretData = map[string][]byte{
"password": []byte(password),
"username": []byte(username),
}
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
)
var (
time = metav1.Now()
regex, _ = regexp.Compile(annotationRegExpString)
)
var tests = []testReconcileItem{
{
testName: "Test Delete Deployment where secret is being used in another deployment's volumes",
deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
DeletionTimestamp: &time,
Finalizers: []string{
finalizer,
},
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
},
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "another-deployment",
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: name,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: name,
},
},
},
},
},
},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Test Delete Deployment where secret is being used in another deployment's container",
deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
DeletionTimestamp: &time,
Finalizers: []string{
finalizer,
},
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
},
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "another-deployment",
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Env: []corev1.EnvVar{
{
Name: name,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: name,
},
Key: passKey,
},
},
},
},
},
},
},
},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Test Delete Deployment",
deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
DeletionTimestamp: &time,
Finalizers: []string{
finalizer,
},
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: nil,
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Test Do not update if Annotations have not changed",
deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
Labels: map[string]string{},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string(nil),
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: "data we don't expect to have updated",
passKey: "data we don't expect to have updated",
},
},
{
testName: "Test Updating Existing Kubernetes Secret using Deployment",
deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: "456",
},
},
Type: corev1.SecretType(""),
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretType(""),
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),
},
},
Type: corev1.SecretType(""),
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
}
func TestReconcileDeployment(t *testing.T) {
for _, testData := range tests {
t.Run(testData.testName, func(t *testing.T) {
// Register operator types with the runtime scheme.
s := scheme.Scheme
s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.deploymentResource)
// Objects to track in the fake client.
objs := []runtime.Object{
testData.deploymentResource,
}
if testData.existingSecret != nil {
objs = append(objs, testData.existingSecret)
}
if testData.existingDeployment != nil {
objs = append(objs, testData.existingDeployment)
}
// Create a fake client to mock API calls.
cl := fake.NewFakeClientWithScheme(s, objs...)
// Create a Deployment object with the scheme and mock kubernetes
// and 1Password Connect client.
opConnectClient := &mocks.TestClient{}
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
item.Version = version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
r := &ReconcileDeployment{
kubeClient: cl,
scheme: s,
opConnectClient: opConnectClient,
opAnnotationRegExp: regex,
}
// Mock request to simulate Reconcile() being called on an event for a
// watched resource .
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: name,
Namespace: namespace,
},
}
_, err := r.Reconcile(req)
assert.Equal(t, testData.expectedError, err)
var expectedSecretName string
if testData.expectedResultSecret == nil {
expectedSecretName = testData.deploymentResource.Name
} else {
expectedSecretName = testData.expectedResultSecret.Name
}
// Check if Secret has been created and has the correct data
secret := &corev1.Secret{}
err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret)
if testData.expectedResultSecret == nil {
assert.Error(t, err)
assert.True(t, errors2.IsNotFound(err))
} else {
assert.Equal(t, testData.expectedResultSecret.Data, secret.Data)
assert.Equal(t, testData.expectedResultSecret.Name, secret.Name)
assert.Equal(t, testData.expectedResultSecret.Type, secret.Type)
assert.Equal(t, testData.expectedResultSecret.Annotations[op.VersionAnnotation], secret.Annotations[op.VersionAnnotation])
updatedCR := &appsv1.Deployment{}
err = cl.Get(context.TODO(), req.NamespacedName, updatedCR)
assert.NoError(t, err)
}
})
}
}
func generateFields(username, password string) []*onepassword.ItemField {
fields := []*onepassword.ItemField{
{
Label: "username",
Value: username,
},
{
Label: "password",
Value: password,
},
}
return fields
}

View File

@@ -1,203 +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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
var 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
err := r.HandleOnePasswordItem(onepassworditem, request)
if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil {
return reconcile.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr)
}
return reconcile.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 reconcile.Result{}, err
}
// Remove finalizer now that cleanup is complete
if err := r.removeFinalizer(onepassworditem); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
func (r *ReconcileOnePasswordItem) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error {
onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer)
if err := r.kubeClient.Update(context.Background(), onePasswordItem); err != nil {
return err
}
return nil
}
func (r *ReconcileOnePasswordItem) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error {
kubernetesSecret := &corev1.Secret{}
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace
r.kubeClient.Delete(context.Background(), kubernetesSecret)
if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil {
if !errors.IsNotFound(err) {
return err
}
}
return nil
}
func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem(opSecret *onepasswordv1.OnePasswordItem) error {
opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer)
return r.kubeClient.Update(context.Background(), opSecret)
}
func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
secretName := resource.GetName()
labels := resource.Labels
secretType := resource.Type
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)
}
// 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.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef)
}
func (r *ReconcileOnePasswordItem) 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.kubeClient.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,
}
}

View File

@@ -1,539 +0,0 @@
package onepassworditem
import (
"context"
"fmt"
"testing"
"github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"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 or VaultPath has not changed",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: "data we don't expect to have updated",
passKey: "data we don't expect to have updated",
},
},
{
testName: "Test Updating Existing Kubernetes Secret using OnePasswordItem",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: "456",
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Test Updating Type of Existing Kubernetes Secret using OnePasswordItem",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
Type: string(corev1.SecretTypeBasicAuth),
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Type: corev1.SecretTypeBasicAuth,
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Type: corev1.SecretTypeBasicAuth,
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,
},
Type: "custom",
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretType("custom"),
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Error if secret type is changed",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
Type: "custom",
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretTypeOpaque,
Data: expectedSecretData,
},
expectedError: kubernetessecrets.ErrCannotUpdateSecretType,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretTypeOpaque,
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Secret from 1Password item with invalid K8s labels",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!my sECReT it3m%",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret-it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Secret from 1Password item with fields and sections that have invalid K8s labels",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!my sECReT it3m%",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret-it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: map[string][]byte{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"ice-cream-type": []byte(iceCream),
},
},
opItem: map[string]string{
userKey: username,
passKey: password,
"first host": firstHost,
"AWS Access Key": awsKey,
"😄 ice-cream type": iceCream,
},
},
{
testName: "Secret from 1Password item with `-`, `_` and `.`",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!.my_sECReT.it3m%-_",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret.it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: map[string][]byte{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"-_ice_cream.type.": []byte(iceCream),
},
},
opItem: map[string]string{
userKey: username,
passKey: password,
"first host": firstHost,
"AWS Access Key": awsKey,
"😄 -_ice_cream.type.": iceCream,
},
},
}
func TestReconcileOnePasswordItem(t *testing.T) {
for _, testData := range tests {
t.Run(testData.testName, func(t *testing.T) {
// Register operator types with the runtime scheme.
s := scheme.Scheme
s.AddKnownTypes(onepasswordv1.SchemeGroupVersion, testData.customResource)
// Objects to track in the fake client.
objs := []runtime.Object{
testData.customResource,
}
if testData.existingSecret != nil {
objs = append(objs, testData.existingSecret)
}
if testData.existingOnePasswordItem != nil {
objs = append(objs, testData.existingOnePasswordItem)
}
// Create a fake client to mock API calls.
cl := fake.NewFakeClientWithScheme(s, objs...)
// Create a OnePasswordItem object with the scheme and mock kubernetes
// and 1Password Connect client.
opConnectClient := &mocks.TestClient{}
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{}
for k, v := range testData.opItem {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
r := &ReconcileOnePasswordItem{
kubeClient: cl,
scheme: s,
opConnectClient: opConnectClient,
}
// Mock request to simulate Reconcile() being called on an event for a
// watched resource .
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: testData.customResource.ObjectMeta.Name,
Namespace: testData.customResource.ObjectMeta.Namespace,
},
}
_, err := r.Reconcile(req)
assert.Equal(t, testData.expectedError, err)
var expectedSecretName string
if testData.expectedResultSecret == nil {
expectedSecretName = testData.customResource.Name
} else {
expectedSecretName = testData.expectedResultSecret.Name
}
// Check if Secret has been created and has the correct data
secret := &corev1.Secret{}
err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret)
if testData.expectedResultSecret == nil {
assert.Error(t, err)
assert.True(t, errors2.IsNotFound(err))
} else {
assert.Equal(t, testData.expectedResultSecret.Data, secret.Data)
assert.Equal(t, testData.expectedResultSecret.Name, secret.Name)
assert.Equal(t, testData.expectedResultSecret.Type, secret.Type)
assert.Equal(t, testData.expectedResultSecret.Annotations[op.VersionAnnotation], secret.Annotations[op.VersionAnnotation])
updatedCR := &onepasswordv1.OnePasswordItem{}
err = cl.Get(context.TODO(), req.NamespacedName, updatedCR)
assert.NoError(t, err)
}
})
}
}
func generateFields(username, password string) []*onepassword.ItemField {
fields := []*onepassword.ItemField{
{
Label: "username",
Value: username,
},
{
Label: "password",
Value: password,
},
}
return fields
}

View File

@@ -7,20 +7,16 @@ import (
"testing" "testing"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation" kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/fake"
) )
const restartDeploymentAnnotation = "false" const restartDeploymentAnnotation = "false"
type k8s struct {
clientset kubernetes.Interface
}
func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
secretName := "test-secret-name" secretName := "test-secret-name"
namespace := "test" namespace := "test"
@@ -31,7 +27,7 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient() kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{} secretLabels := map[string]string{}
secretType := "" secretType := ""
@@ -59,7 +55,7 @@ func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient() kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{} secretLabels := map[string]string{}
secretType := "" secretType := ""
@@ -104,7 +100,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient() kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{} secretLabels := map[string]string{}
secretType := "" secretType := ""
@@ -219,7 +215,7 @@ func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient() kubeClient := fake.NewClientBuilder().Build()
secretLabels := map[string]string{} secretLabels := map[string]string{}
secretType := "kubernetes.io/tls" secretType := "kubernetes.io/tls"

View File

@@ -5,80 +5,147 @@ import (
) )
type TestClient struct { type TestClient struct {
GetVaultsFunc func() ([]onepassword.Vault, error) GetVaultsFunc func() ([]onepassword.Vault, error)
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetVaultFunc func(uuid string) (*onepassword.Vault, error) GetVaultFunc func(uuid string) (*onepassword.Vault, error)
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error) GetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error)
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error) GetVaultByTitleFunc func(title string) (*onepassword.Vault, error)
GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error) GetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error)
GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error) GetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error)
CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) GetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error)
UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) GetItemsFunc func(vaultQuery string) ([]onepassword.Item, error)
DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error GetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error)
GetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) CreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
GetFileContentFunc func(file *onepassword.File) ([]byte, 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 ( var (
GetGetVaultsFunc func() ([]onepassword.Vault, error) DoGetVaultsFunc func() ([]onepassword.Vault, error)
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
DoGetVaultFunc func(uuid string) (*onepassword.Vault, error) DoGetVaultFunc func(uuid string) (*onepassword.Vault, error)
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error) DoGetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error)
DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error) DoGetVaultByTitleFunc func(title string) (*onepassword.Vault, error)
DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error) DoGetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error)
DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) DoGetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error)
DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error DoGetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error)
DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error) DoGetItemsFunc func(vaultQuery string) ([]onepassword.Item, error)
DoUpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) DoGetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error)
DoGetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) DoCreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
DoGetFileContentFunc func(file *onepassword.File) ([]byte, 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 // Do is the mock client's `Do` func
func (m *TestClient) GetVaults() ([]onepassword.Vault, error) { func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
return GetGetVaultsFunc() return DoGetVaultsFunc()
} }
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
return DoGetVaultsByTitleFunc(title) return DoGetVaultsByTitleFunc(title)
} }
func (m *TestClient) GetVault(uuid string) (*onepassword.Vault, error) { func (m *TestClient) GetVault(vaultQuery string) (*onepassword.Vault, error) {
return DoGetVaultFunc(uuid) return DoGetVaultFunc(vaultQuery)
} }
func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) { func (m *TestClient) GetVaultByUUID(uuid string) (*onepassword.Vault, error) {
return GetGetItemFunc(uuid, vaultUUID) return DoGetVaultByUUIDFunc(uuid)
} }
func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) { func (m *TestClient) GetVaultByTitle(title string) (*onepassword.Vault, error) {
return DoGetItemsFunc(vaultUUID) return DoGetVaultByTitleFunc(title)
} }
func (m *TestClient) GetItemsByTitle(title, vaultUUID string) ([]onepassword.Item, error) { func (m *TestClient) GetItem(itemQuery string, vaultQuery string) (*onepassword.Item, error) {
return DoGetItemsByTitleFunc(title, vaultUUID) return DoGetItemFunc(itemQuery, vaultQuery)
} }
func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) { func (m *TestClient) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) {
return DoGetItemByTitleFunc(title, vaultUUID) return DoGetItemByUUIDFunc(uuid, vaultQuery)
} }
func (m *TestClient) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) { func (m *TestClient) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) {
return DoCreateItemFunc(item, vaultUUID) return DoGetItemByTitleFunc(title, vaultQuery)
} }
func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error { func (m *TestClient) GetItems(vaultQuery string) ([]onepassword.Item, error) {
return DoDeleteItemFunc(item, vaultUUID) return DoGetItemsFunc(vaultQuery)
} }
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) { func (m *TestClient) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) {
return DoUpdateItemFunc(item, vaultUUID) return DoGetItemsByTitleFunc(title, vaultQuery)
} }
func (m *TestClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) { func (m *TestClient) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
return DoGetFileFunc(uuid, itemUUID, vaultUUID) 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) { func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) {
return DoGetFileContentFunc(file) return DoGetFileContentFunc(file)
} }
func (m *TestClient) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) {
return DoDownloadFileFunc(file, targetDirectory, overwrite)
}
func (m *TestClient) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error {
return DoLoadStructFromItemByUUIDFunc(config, itemUUID, vaultQuery)
}
func (m *TestClient) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error {
return DoLoadStructFromItemByTitleFunc(config, itemTitle, vaultQuery)
}
func (m *TestClient) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error {
return DoLoadStructFromItemFunc(config, itemQuery, vaultQuery)
}
func (m *TestClient) LoadStruct(config interface{}) error {
return DoLoadStructFunc(config)
}

View File

@@ -2,12 +2,12 @@ package onepassword
import ( import (
"context" "context"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"os" "os"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/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/types"
"k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@@ -15,8 +15,8 @@ import (
) )
var logConnectSetup = logf.Log.WithName("ConnectSetup") var logConnectSetup = logf.Log.WithName("ConnectSetup")
var deploymentPath = "deploy/connect/deployment.yaml" var deploymentPath = "config/connect/deployment.yaml"
var servicePath = "deploy/connect/service.yaml" var servicePath = "config/connect/service.yaml"
func SetupConnect(kubeClient client.Client, deploymentNamespace string) error { func SetupConnect(kubeClient client.Client, deploymentNamespace string) error {
err := setupService(kubeClient, servicePath, deploymentNamespace) err := setupService(kubeClient, servicePath, deploymentNamespace)

View File

@@ -23,9 +23,9 @@ func TestServiceSetup(t *testing.T) {
objs := []runtime.Object{} objs := []runtime.Object{}
// Create a fake client to mock API calls. // 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 { if err != nil {
t.Errorf("Error Setting Up Connect: %v", err) t.Errorf("Error Setting Up Connect: %v", err)
@@ -48,9 +48,9 @@ func TestDeploymentSetup(t *testing.T) {
objs := []runtime.Object{} objs := []runtime.Object{}
// Create a fake client to mock API calls. // 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 { if err != nil {
t.Errorf("Error Setting Up Connect: %v", err) t.Errorf("Error Setting Up Connect: %v", err)

View File

@@ -9,8 +9,8 @@ import (
func TestAreContainersUsingSecretsFromEnv(t *testing.T) { func TestAreContainersUsingSecretsFromEnv(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{ secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{}, "onepassword-database-secret": {},
"onepassword-api-key": &corev1.Secret{}, "onepassword-api-key": {},
} }
containerSecretNames := []string{ containerSecretNames := []string{

View File

@@ -9,8 +9,8 @@ import (
func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) { func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{ secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{}, "onepassword-database-secret": {},
"onepassword-api-key": &corev1.Secret{}, "onepassword-api-key": {},
} }
volumeSecretNames := []string{ volumeSecretNames := []string{
@@ -28,8 +28,8 @@ func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) {
func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{ secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{}, "onepassword-database-secret": {},
"onepassword-api-key": &corev1.Secret{}, "onepassword-api-key": {},
} }
containerSecretNames := []string{ containerSecretNames := []string{
@@ -47,8 +47,8 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) {
func TestIsDeploymentNotUSingSecrets(t *testing.T) { func TestIsDeploymentNotUSingSecrets(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{ secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{}, "onepassword-database-secret": {},
"onepassword-api-key": &corev1.Secret{}, "onepassword-api-key": {},
} }
deployment := &appsv1.Deployment{} deployment := &appsv1.Deployment{}

View File

@@ -6,6 +6,7 @@ import (
"github.com/1Password/connect-sdk-go/connect" "github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
) )

View File

@@ -5,8 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
v1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/utils" "github.com/1Password/onepassword-operator/pkg/utils"
@@ -194,7 +193,7 @@ func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string
} }
func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string { func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string {
onePasswordItem := &v1.OnePasswordItem{} onePasswordItem := &onepasswordv1.OnePasswordItem{}
// Search for our original OnePasswordItem if it exists // Search for our original OnePasswordItem if it exists
err := h.client.Get(context.TODO(), client.ObjectKey{ err := h.client.Get(context.TODO(), client.ObjectKey{

View File

@@ -800,10 +800,10 @@ func TestUpdateSecretHandler(t *testing.T) {
} }
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
cl := fake.NewFakeClientWithScheme(s, objs...) cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
opConnectClient := &mocks.TestClient{} 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 := onepassword.Item{}
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
@@ -871,7 +871,7 @@ func TestIsUpdatedSecret(t *testing.T) {
secretName := "test-secret" secretName := "test-secret"
updatedSecrets := map[string]*corev1.Secret{ updatedSecrets := map[string]*corev1.Secret{
"some_secret": &corev1.Secret{}, "some_secret": {},
} }
assert.False(t, isUpdatedSecret(secretName, updatedSecrets)) assert.False(t, isUpdatedSecret(secretName, updatedSecrets))

View File

@@ -8,8 +8,8 @@ import (
func TestAreVolmesUsingSecrets(t *testing.T) { func TestAreVolmesUsingSecrets(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{ secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{}, "onepassword-database-secret": {},
"onepassword-api-key": &corev1.Secret{}, "onepassword-api-key": {},
} }
volumeSecretNames := []string{ volumeSecretNames := []string{
@@ -27,8 +27,8 @@ func TestAreVolmesUsingSecrets(t *testing.T) {
func TestAreVolumesNotUsingSecrets(t *testing.T) { func TestAreVolumesNotUsingSecrets(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{ secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{}, "onepassword-database-secret": {},
"onepassword-api-key": &corev1.Secret{}, "onepassword-api-key": {},
} }
volumeSecretNames := []string{ volumeSecretNames := []string{

63
pkg/utils/k8sutil.go Normal file
View 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)
}

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
// metadata and API service accounts. // metadata and API service accounts.
// //
// This package is a wrapper around the GCE metadata service, // This package is a wrapper around the GCE metadata service,
// as documented at https://developers.google.com/compute/docs/metadata. // as documented at https://cloud.google.com/compute/docs/metadata/overview.
package metadata // import "cloud.google.com/go/compute/metadata" package metadata // import "cloud.google.com/go/compute/metadata"
import ( import (
@@ -61,25 +61,19 @@ var (
instID = &cachedValue{k: "instance/id", trim: true} instID = &cachedValue{k: "instance/id", trim: true}
) )
var ( var defaultClient = &Client{hc: newDefaultHTTPClient()}
defaultClient = &Client{hc: &http.Client{
Transport: &http.Transport{ func newDefaultHTTPClient() *http.Client {
Dial: (&net.Dialer{ return &http.Client{
Timeout: 2 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
ResponseHeaderTimeout: 2 * time.Second,
},
}}
subscribeClient = &Client{hc: &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
Dial: (&net.Dialer{ Dial: (&net.Dialer{
Timeout: 2 * time.Second, Timeout: 2 * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
}).Dial, }).Dial,
}, },
}} Timeout: 5 * time.Second,
) }
}
// NotDefinedError is returned when requested metadata is not defined. // NotDefinedError is returned when requested metadata is not defined.
// //
@@ -141,7 +135,7 @@ func testOnGCE() bool {
go func() { go func() {
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil) req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
req.Header.Set("User-Agent", userAgent) req.Header.Set("User-Agent", userAgent)
res, err := defaultClient.hc.Do(req.WithContext(ctx)) res, err := newDefaultHTTPClient().Do(req.WithContext(ctx))
if err != nil { if err != nil {
resc <- false resc <- false
return return
@@ -151,7 +145,8 @@ func testOnGCE() bool {
}() }()
go func() { go func() {
addrs, err := net.LookupHost("metadata.google.internal") resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal")
if err != nil || len(addrs) == 0 { if err != nil || len(addrs) == 0 {
resc <- false resc <- false
return return
@@ -206,10 +201,9 @@ func systemInfoSuggestsGCE() bool {
return name == "Google" || name == "Google Compute Engine" return name == "Google" || name == "Google Compute Engine"
} }
// Subscribe calls Client.Subscribe on a client designed for subscribing (one with no // Subscribe calls Client.Subscribe on the default client.
// ResponseHeaderTimeout).
func Subscribe(suffix string, fn func(v string, ok bool) error) error { func Subscribe(suffix string, fn func(v string, ok bool) error) error {
return subscribeClient.Subscribe(suffix, fn) return defaultClient.Subscribe(suffix, fn)
} }
// Get calls Client.Get on the default client. // Get calls Client.Get on the default client.
@@ -280,15 +274,21 @@ type Client struct {
hc *http.Client hc *http.Client
} }
// NewClient returns a Client that can be used to fetch metadata. All HTTP requests // NewClient returns a Client that can be used to fetch metadata.
// will use the given http.Client instead of the default client. // Returns the client that uses the specified http.Client for HTTP requests.
// If nil is specified, returns the default client.
func NewClient(c *http.Client) *Client { func NewClient(c *http.Client) *Client {
if c == nil {
return defaultClient
}
return &Client{hc: c} return &Client{hc: c}
} }
// getETag returns a value from the metadata service as well as the associated ETag. // getETag returns a value from the metadata service as well as the associated ETag.
// This func is otherwise equivalent to Get. // This func is otherwise equivalent to Get.
func (c *Client) getETag(suffix string) (value, etag string, err error) { func (c *Client) getETag(suffix string) (value, etag string, err error) {
ctx := context.TODO()
// Using a fixed IP makes it very difficult to spoof the metadata service in // 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 // a container, which is an important use-case for local testing of cloud
// deployments. To enable spoofing of the metadata service, the environment // deployments. To enable spoofing of the metadata service, the environment
@@ -303,14 +303,34 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
// being stable anyway. // being stable anyway.
host = metadataIP host = metadataIP
} }
suffix = strings.TrimLeft(suffix, "/")
u := "http://" + host + "/computeMetadata/v1/" + suffix u := "http://" + host + "/computeMetadata/v1/" + suffix
req, _ := http.NewRequest("GET", u, nil) req, err := 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 { if err != nil {
return "", "", err return "", "", err
} }
req.Header.Set("Metadata-Flavor", "Google")
req.Header.Set("User-Agent", userAgent)
var res *http.Response
var reqErr error
retryer := newRetryer()
for {
res, reqErr = c.hc.Do(req)
var code int
if res != nil {
code = res.StatusCode
}
if delay, shouldRetry := retryer.Retry(code, reqErr); shouldRetry {
if err := sleep(ctx, delay); err != nil {
return "", "", err
}
continue
}
break
}
if reqErr != nil {
return "", "", reqErr
}
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode == http.StatusNotFound { if res.StatusCode == http.StatusNotFound {
return "", "", NotDefinedError(suffix) return "", "", NotDefinedError(suffix)
@@ -407,11 +427,7 @@ func (c *Client) InstanceTags() ([]string, error) {
// InstanceName returns the current VM's instance ID string. // InstanceName returns the current VM's instance ID string.
func (c *Client) InstanceName() (string, error) { func (c *Client) InstanceName() (string, error) {
host, err := c.Hostname() return c.getTrimmed("instance/name")
if err != nil {
return "", err
}
return strings.Split(host, ".")[0], nil
} }
// Zone returns the current VM's zone, such as "us-central1-b". // Zone returns the current VM's zone, such as "us-central1-b".

114
vendor/cloud.google.com/go/compute/metadata/retry.go generated vendored Normal file
View File

@@ -0,0 +1,114 @@
// Copyright 2021 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
import (
"context"
"io"
"math/rand"
"net/http"
"time"
)
const (
maxRetryAttempts = 5
)
var (
syscallRetryable = func(err error) bool { return false }
)
// defaultBackoff is basically equivalent to gax.Backoff without the need for
// the dependency.
type defaultBackoff struct {
max time.Duration
mul float64
cur time.Duration
}
func (b *defaultBackoff) Pause() time.Duration {
d := time.Duration(1 + rand.Int63n(int64(b.cur)))
b.cur = time.Duration(float64(b.cur) * b.mul)
if b.cur > b.max {
b.cur = b.max
}
return d
}
// sleep is the equivalent of gax.Sleep without the need for the dependency.
func sleep(ctx context.Context, d time.Duration) error {
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
return ctx.Err()
case <-t.C:
return nil
}
}
func newRetryer() *metadataRetryer {
return &metadataRetryer{bo: &defaultBackoff{
cur: 100 * time.Millisecond,
max: 30 * time.Second,
mul: 2,
}}
}
type backoff interface {
Pause() time.Duration
}
type metadataRetryer struct {
bo backoff
attempts int
}
func (r *metadataRetryer) Retry(status int, err error) (time.Duration, bool) {
if status == http.StatusOK {
return 0, false
}
retryOk := shouldRetry(status, err)
if !retryOk {
return 0, false
}
if r.attempts == maxRetryAttempts {
return 0, false
}
r.attempts++
return r.bo.Pause(), true
}
func shouldRetry(status int, err error) bool {
if 500 <= status && status <= 599 {
return true
}
if err == io.ErrUnexpectedEOF {
return true
}
// Transient network errors should be retried.
if syscallRetryable(err) {
return true
}
if err, ok := err.(interface{ Temporary() bool }); ok {
if err.Temporary() {
return true
}
}
if err, ok := err.(interface{ Unwrap() error }); ok {
return shouldRetry(status, err.Unwrap())
}
return false
}

View File

@@ -0,0 +1,26 @@
// Copyright 2021 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.
//go:build linux
// +build linux
package metadata
import "syscall"
func init() {
// Initialize syscallRetryable to return true on transient socket-level
// errors. These errors are specific to Linux.
syscallRetryable = func(err error) bool { return err == syscall.ECONNRESET || err == syscall.ECONNREFUSED }
}

View File

@@ -3,15 +3,17 @@ package connect
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"reflect"
"regexp"
opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext" "github.com/opentracing/opentracing-go/ext"
jaegerClientConfig "github.com/uber/jaeger-client-go/config" jaegerClientConfig "github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-client-go/zipkin" "github.com/uber/jaeger-client-go/zipkin"
@@ -23,20 +25,37 @@ const (
defaultUserAgent = "connect-sdk-go/%s" 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 // Client Represents an available 1Password Connect API to connect to
type Client interface { type Client interface {
GetVaults() ([]onepassword.Vault, error) GetVaults() ([]onepassword.Vault, error)
GetVault(uuid string) (*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) GetVaultsByTitle(uuid string) ([]onepassword.Vault, error)
GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) GetItems(vaultQuery string) ([]onepassword.Item, error)
GetItems(vaultUUID string) ([]onepassword.Item, error) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error)
GetItemsByTitle(title string, vaultUUID string) ([]onepassword.Item, error) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error)
GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error)
CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error)
UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
DeleteItem(item *onepassword.Item, vaultUUID string) error UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
GetFile(fileUUID string, itemUUID string, vaultUUID string) (*onepassword.File, 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) 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 { type httpClient interface {
@@ -125,13 +144,26 @@ func (rs *restClient) GetVaults() ([]onepassword.Vault, error) {
return vaults, nil return vaults, nil
} }
// GetVaults Get a list of all available vaults // GetVault Get a vault based on its name or ID
func (rs *restClient) GetVault(uuid string) (*onepassword.Vault, error) { func (rs *restClient) GetVault(vaultQuery string) (*onepassword.Vault, error) {
if uuid == "" { span := rs.tracer.StartSpan("GetVault")
return nil, errors.New("no uuid provided") 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("GetVault") span := rs.tracer.StartSpan("GetVaultByUUID")
defer span.Finish() defer span.Finish()
vaultURL := fmt.Sprintf("/v1/vaults/%s", uuid) vaultURL := fmt.Sprintf("/v1/vaults/%s", uuid)
@@ -152,6 +184,22 @@ func (rs *restClient) GetVault(uuid string) (*onepassword.Vault, error) {
return &vault, nil 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) { func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
span := rs.tracer.StartSpan("GetVaultsByTitle") span := rs.tracer.StartSpan("GetVaultsByTitle")
defer span.Finish() defer span.Finish()
@@ -176,11 +224,48 @@ func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error
return vaults, nil return vaults, nil
} }
// GetItem Get a specific Item from the 1Password Connect API func (rs *restClient) getVaultUUID(vaultQuery string) (string, error) {
func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, 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") span := rs.tracer.StartSpan("GetItem")
defer span.Finish() 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) itemURL := fmt.Sprintf("/v1/vaults/%s/items/%s", vaultUUID, uuid)
request, err := rs.buildRequest(http.MethodGet, itemURL, http.NoBody, span) request, err := rs.buildRequest(http.MethodGet, itemURL, http.NoBody, span)
if err != nil { if err != nil {
@@ -199,7 +284,12 @@ func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item,
return &item, nil 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") span := rs.tracer.StartSpan("GetItemByTitle")
defer span.Finish() defer span.Finish()
items, err := rs.GetItemsByTitle(title, vaultUUID) items, err := rs.GetItemsByTitle(title, vaultUUID)
@@ -211,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 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") span := rs.tracer.StartSpan("GetItemsByTitle")
defer span.Finish() defer span.Finish()
@@ -230,15 +325,29 @@ func (rs *restClient) GetItemsByTitle(title string, vaultUUID string) ([]onepass
return nil, err return nil, err
} }
var items []onepassword.Item var itemSummaries []onepassword.Item
if err := parseResponse(response, http.StatusOK, &items); err != nil { if err := parseResponse(response, http.StatusOK, &itemSummaries); err != nil {
return nil, err 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 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") span := rs.tracer.StartSpan("GetItems")
defer span.Finish() defer span.Finish()
@@ -261,8 +370,27 @@ func (rs *restClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
return items, nil 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 // 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") span := rs.tracer.StartSpan("CreateItem")
defer span.Finish() defer span.Finish()
@@ -342,9 +470,98 @@ func (rs *restClient) DeleteItem(item *onepassword.Item, vaultUUID string) error
return nil 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. // GetFile Get a specific File in a specified item.
// This does not include the file contents. Call GetFileContent() to load the file's content. // This does not include the file contents. Call GetFileContent() to load the file's content.
func (rs *restClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) { 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") span := rs.tracer.StartSpan("GetFile")
defer span.Finish() defer span.Finish()
@@ -376,7 +593,53 @@ func (rs *restClient) GetFileContent(file *onepassword.File) ([]byte, error) {
if content, err := file.Content(); err == nil { if content, err := file.Content(); err == nil {
return content, 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") span := rs.tracer.StartSpan("GetFileContent")
defer span.Finish() defer span.Finish()
@@ -392,14 +655,19 @@ func (rs *restClient) GetFileContent(file *onepassword.File) ([]byte, error) {
if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil { if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil {
return nil, err return nil, err
} }
return response, nil
}
content, err := readResponseBody(response, http.StatusOK) func createFile(path string) (*os.File, error) {
osFile, err := os.Create(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = os.Chmod(path, 0600)
file.SetContent(content) if err != nil {
return content, nil return nil, err
}
return osFile, nil
} }
func (rs *restClient) buildRequest(method string, path string, body io.Reader, span opentracing.Span) (*http.Request, error) { func (rs *restClient) buildRequest(method string, path string, body io.Reader, span opentracing.Span) (*http.Request, error) {
@@ -423,6 +691,140 @@ func (rs *restClient) buildRequest(method string, path string, body io.Reader, s
return request, nil 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 { func parseResponse(resp *http.Response, expectedStatusCode int, result interface{}) error {
body, err := readResponseBody(resp, expectedStatusCode) body, err := readResponseBody(resp, expectedStatusCode)
if err != nil { if err != nil {
@@ -451,3 +853,8 @@ func readResponseBody(resp *http.Response, expectedStatusCode int) ([]byte, erro
} }
return body, nil return body, nil
} }
func isValidUUID(u string) bool {
r := regexp.MustCompile("^[a-z0-9]{26}$")
return r.MatchString(u)
}

View File

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

View 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 ""
}

View File

@@ -10,7 +10,7 @@ import (
// SDKVersion is the latest Semantic Version of the library // SDKVersion is the latest Semantic Version of the library
// Do not rename this variable without changing the regex in the Makefile // Do not rename this variable without changing the regex in the Makefile
const SDKVersion = "1.2.0" const SDKVersion = "1.5.0"
const VersionHeaderKey = "1Password-Connect-Version" const VersionHeaderKey = "1Password-Connect-Version"

View File

@@ -12,6 +12,7 @@ type ItemCategory string
const ( const (
Login ItemCategory = "LOGIN" Login ItemCategory = "LOGIN"
Password ItemCategory = "PASSWORD" Password ItemCategory = "PASSWORD"
ApiCredential ItemCategory = "API_CREDENTIAL"
Server ItemCategory = "SERVER" Server ItemCategory = "SERVER"
Database ItemCategory = "DATABASE" Database ItemCategory = "DATABASE"
CreditCard ItemCategory = "CREDIT_CARD" CreditCard ItemCategory = "CREDIT_CARD"
@@ -28,7 +29,8 @@ const (
Document ItemCategory = "DOCUMENT" Document ItemCategory = "DOCUMENT"
EmailAccount ItemCategory = "EMAIL_ACCOUNT" EmailAccount ItemCategory = "EMAIL_ACCOUNT"
SocialSecurityNumber ItemCategory = "SOCIAL_SECURITY_NUMBER" SocialSecurityNumber ItemCategory = "SOCIAL_SECURITY_NUMBER"
ApiCredential ItemCategory = "API_CREDENTIAL" MedicalRecord ItemCategory = "MEDICAL_RECORD"
SSHKey ItemCategory = "SSH_KEY"
Custom ItemCategory = "CUSTOM" Custom ItemCategory = "CUSTOM"
) )
@@ -40,7 +42,7 @@ func (ic *ItemCategory) UnmarshalJSON(b []byte) error {
switch category { switch category {
case Login, Password, Server, Database, CreditCard, Membership, Passport, SoftwareLicense, case Login, Password, Server, Database, CreditCard, Membership, Passport, SoftwareLicense,
OutdoorLicense, SecureNote, WirelessRouter, BankAccount, DriverLicense, Identity, RewardProgram, OutdoorLicense, SecureNote, WirelessRouter, BankAccount, DriverLicense, Identity, RewardProgram,
Document, EmailAccount, SocialSecurityNumber, ApiCredential: Document, EmailAccount, SocialSecurityNumber, ApiCredential, MedicalRecord, SSHKey:
*ic = category *ic = category
default: default:
*ic = Custom *ic = Custom
@@ -58,7 +60,6 @@ type Item struct {
Favorite bool `json:"favorite,omitempty"` Favorite bool `json:"favorite,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
Version int `json:"version,omitempty"` Version int `json:"version,omitempty"`
Trashed bool `json:"trashed,omitempty"`
Vault ItemVault `json:"vault"` Vault ItemVault `json:"vault"`
Category ItemCategory `json:"category,omitempty"` // TODO: switch this to `category` Category ItemCategory `json:"category,omitempty"` // TODO: switch this to `category`
@@ -70,6 +71,9 @@ type Item struct {
LastEditedBy string `json:"lastEditedBy,omitempty"` LastEditedBy string `json:"lastEditedBy,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"` CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt time.Time `json:"updatedAt,omitempty"` UpdatedAt time.Time `json:"updatedAt,omitempty"`
// Deprecated: Connect does not return trashed items.
Trashed bool `json:"trashed,omitempty"`
} }
// ItemVault represents the Vault the Item is found in // ItemVault represents the Vault the Item is found in
@@ -80,6 +84,7 @@ type ItemVault struct {
// ItemURL is a simplified item URL // ItemURL is a simplified item URL
type ItemURL struct { type ItemURL struct {
Primary bool `json:"primary,omitempty"` Primary bool `json:"primary,omitempty"`
Label string `json:"label,omitempty"`
URL string `json:"href"` URL string `json:"href"`
} }
@@ -91,8 +96,9 @@ type ItemSection struct {
// GeneratorRecipe Representation of a "recipe" used to generate a field // GeneratorRecipe Representation of a "recipe" used to generate a field
type GeneratorRecipe struct { type GeneratorRecipe struct {
Length int `json:"length,omitempty"` Length int `json:"length,omitempty"`
CharacterSets []string `json:"characterSets,omitempty"` CharacterSets []string `json:"characterSets,omitempty"`
ExcludeCharacters string `json:"excludeCharacters,omitempty"`
} }
// ItemField Representation of a single field on an Item // ItemField Representation of a single field on an Item
@@ -106,9 +112,10 @@ type ItemField struct {
Generate bool `json:"generate,omitempty"` Generate bool `json:"generate,omitempty"`
Recipe *GeneratorRecipe `json:"recipe,omitempty"` Recipe *GeneratorRecipe `json:"recipe,omitempty"`
Entropy float64 `json:"entropy,omitempty"` Entropy float64 `json:"entropy,omitempty"`
TOTP string `json:"totp,omitempty"`
} }
// Get Retrieve the value of a field on the item by its label. To specify a // GetValue Retrieve the value of a field on the item by its label. To specify a
// field from a specific section pass in <section label>.<field label>. If // field from a specific section pass in <section label>.<field label>. If
// no field matching the selector is found return "". // no field matching the selector is found return "".
func (i *Item) GetValue(field string) string { func (i *Item) GetValue(field string) string {

View File

@@ -1,3 +1,4 @@
# The standard Go .gitignore file follows. (Sourced from: github.com/github/gitignore/master/Go.gitignore)
# Compiled Object files, Static and Dynamic libs (Shared Objects) # Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o *.o
*.a *.a
@@ -6,6 +7,9 @@
# Folders # Folders
_obj _obj
_test _test
.DS_Store
.idea/
.vscode/
# Architecture specific extensions/prefixes # Architecture specific extensions/prefixes
*.[568vq] *.[568vq]
@@ -21,3 +25,8 @@ _testmain.go
*.exe *.exe
*.test *.test
*.prof
# go-autorest specific
vendor/
autorest/azure/example/example

1004
vendor/github.com/Azure/go-autorest/CHANGELOG.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

23
vendor/github.com/Azure/go-autorest/GNUmakefile generated vendored Normal file
View File

@@ -0,0 +1,23 @@
DIR?=./autorest/
default: build
build: fmt
go install $(DIR)
test:
go test $(DIR) || exit 1
vet:
@echo "go vet ."
@go vet $(DIR)... ; if [ $$? -eq 1 ]; then \
echo ""; \
echo "Vet found suspicious constructs. Please check the reported constructs"; \
echo "and fix them if necessary before submitting the code for review."; \
exit 1; \
fi
fmt:
gofmt -w $(DIR)
.PHONY: build test vet fmt

324
vendor/github.com/Azure/go-autorest/Gopkg.lock generated vendored Normal file
View File

@@ -0,0 +1,324 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:892e39e5c083d0943f1e80ab8351690f183c6a5ab24e1d280adcad424c26255e"
name = "contrib.go.opencensus.io/exporter/ocagent"
packages = ["."]
pruneopts = "UT"
revision = "a8a6f458bbc1d5042322ad1f9b65eeb0b69be9ea"
version = "v0.6.0"
[[projects]]
digest = "1:8f5acd4d4462b5136af644d25101f0968a7a94ee90fcb2059cec5b7cc42e0b20"
name = "github.com/census-instrumentation/opencensus-proto"
packages = [
"gen-go/agent/common/v1",
"gen-go/agent/metrics/v1",
"gen-go/agent/trace/v1",
"gen-go/metrics/v1",
"gen-go/resource/v1",
"gen-go/trace/v1",
]
pruneopts = "UT"
revision = "d89fa54de508111353cb0b06403c00569be780d8"
version = "v0.2.1"
[[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
pruneopts = "UT"
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
digest = "1:cf0d2e435fd4ce45b789e93ef24b5f08e86be0e9807a16beb3694e2d8c9af965"
name = "github.com/dimchansky/utfbom"
packages = ["."]
pruneopts = "UT"
revision = "d2133a1ce379ef6fa992b0514a77146c60db9d1c"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:b7cb6054d3dff43b38ad2e92492f220f57ae6087ee797dca298139776749ace8"
name = "github.com/golang/groupcache"
packages = ["lru"]
pruneopts = "UT"
revision = "611e8accdfc92c4187d399e95ce826046d4c8d73"
[[projects]]
digest = "1:e3839df32927e8d3403cd5aa7253d966e8ff80fc8f10e2e35d146461cd83fcfa"
name = "github.com/golang/protobuf"
packages = [
"descriptor",
"jsonpb",
"proto",
"protoc-gen-go/descriptor",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/struct",
"ptypes/timestamp",
"ptypes/wrappers",
]
pruneopts = "UT"
revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7"
version = "v1.3.2"
[[projects]]
digest = "1:c560cd79300fac84f124b96225181a637a70b60155919a3c36db50b7cca6b806"
name = "github.com/grpc-ecosystem/grpc-gateway"
packages = [
"internal",
"runtime",
"utilities",
]
pruneopts = "UT"
revision = "f7120437bb4f6c71f7f5076ad65a45310de2c009"
version = "v1.12.1"
[[projects]]
digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "UT"
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
version = "v1.1.0"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:99d32780e5238c2621fff621123997c3e3cca96db8be13179013aea77dfab551"
name = "github.com/stretchr/testify"
packages = [
"assert",
"require",
]
pruneopts = "UT"
revision = "221dbe5ed46703ee255b1da0dec05086f5035f62"
version = "v1.4.0"
[[projects]]
digest = "1:7c5e00383399fe13de0b4b65c9fdde16275407ce8ac02d867eafeaa916edcc71"
name = "go.opencensus.io"
packages = [
".",
"internal",
"internal/tagencoding",
"metric/metricdata",
"metric/metricproducer",
"plugin/ocgrpc",
"plugin/ochttp",
"plugin/ochttp/propagation/b3",
"plugin/ochttp/propagation/tracecontext",
"resource",
"stats",
"stats/internal",
"stats/view",
"tag",
"trace",
"trace/internal",
"trace/propagation",
"trace/tracestate",
]
pruneopts = "UT"
revision = "aad2c527c5defcf89b5afab7f37274304195a6b2"
version = "v0.22.2"
[[projects]]
branch = "master"
digest = "1:f604f5e2ee721b6757d962dfe7bab4f28aae50c456e39cfb2f3819762a44a6ae"
name = "golang.org/x/crypto"
packages = [
"pkcs12",
"pkcs12/internal/rc2",
]
pruneopts = "UT"
revision = "e9b2fee46413994441b28dfca259d911d963dfed"
[[projects]]
branch = "master"
digest = "1:334b27eac455cb6567ea28cd424230b07b1a64334a2f861a8075ac26ce10af43"
name = "golang.org/x/lint"
packages = [
".",
"golint",
]
pruneopts = "UT"
revision = "fdd1cda4f05fd1fd86124f0ef9ce31a0b72c8448"
[[projects]]
branch = "master"
digest = "1:257a75d024975428ab9192bfc334c3490882f8cb21322ea5784ca8eca000a910"
name = "golang.org/x/net"
packages = [
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/timeseries",
"trace",
]
pruneopts = "UT"
revision = "1ddd1de85cb0337b623b740a609d35817d516a8d"
[[projects]]
branch = "master"
digest = "1:382bb5a7fb4034db3b6a2d19e5a4a6bcf52f4750530603c01ca18a172fa3089b"
name = "golang.org/x/sync"
packages = ["semaphore"]
pruneopts = "UT"
revision = "cd5d95a43a6e21273425c7ae415d3df9ea832eeb"
[[projects]]
branch = "master"
digest = "1:4da420ceda5f68e8d748aa2169d0ed44ffadb1bbd6537cf778a49563104189b8"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "ce4227a45e2eb77e5c847278dcc6a626742e2945"
[[projects]]
digest = "1:8d8faad6b12a3a4c819a3f9618cb6ee1fa1cfc33253abeeea8b55336721e3405"
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/language",
"internal/language/compact",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
]
pruneopts = "UT"
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
version = "v0.3.2"
[[projects]]
branch = "master"
digest = "1:4eb5ea8395fb60212dd58b92c9db80bab59d5e99c7435f9a6a0a528c373b60e7"
name = "golang.org/x/tools"
packages = [
"go/ast/astutil",
"go/gcexportdata",
"go/internal/gcimporter",
"go/types/typeutil",
]
pruneopts = "UT"
revision = "259af5ff87bdcd4abf2ecda8edc3f13f04f26a42"
[[projects]]
digest = "1:964bb30febc27fabfbec4759fa530c6ec35e77a7c85fed90b9317ea39a054877"
name = "google.golang.org/api"
packages = ["support/bundler"]
pruneopts = "UT"
revision = "8a410c21381766a810817fd6200fce8838ecb277"
version = "v0.14.0"
[[projects]]
branch = "master"
digest = "1:a8d5c2c6e746b3485e36908ab2a9e3d77b86b81f8156d88403c7d2b462431dfd"
name = "google.golang.org/genproto"
packages = [
"googleapis/api/httpbody",
"googleapis/rpc/status",
"protobuf/field_mask",
]
pruneopts = "UT"
revision = "51378566eb590fa106d1025ea12835a4416dda84"
[[projects]]
digest = "1:b59ce3ddb11daeeccccc9cb3183b58ebf8e9a779f1c853308cd91612e817a301"
name = "google.golang.org/grpc"
packages = [
".",
"backoff",
"balancer",
"balancer/base",
"balancer/roundrobin",
"binarylog/grpc_binarylog_v1",
"codes",
"connectivity",
"credentials",
"credentials/internal",
"encoding",
"encoding/proto",
"grpclog",
"internal",
"internal/backoff",
"internal/balancerload",
"internal/binarylog",
"internal/buffer",
"internal/channelz",
"internal/envconfig",
"internal/grpcrand",
"internal/grpcsync",
"internal/resolver/dns",
"internal/resolver/passthrough",
"internal/syscall",
"internal/transport",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"serviceconfig",
"stats",
"status",
"tap",
]
pruneopts = "UT"
revision = "1a3960e4bd028ac0cec0a2afd27d7d8e67c11514"
version = "v1.25.1"
[[projects]]
digest = "1:b75b3deb2bce8bc079e16bb2aecfe01eb80098f5650f9e93e5643ca8b7b73737"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "1f64d6156d11335c3f22d9330b0ad14fc1e789ce"
version = "v2.2.7"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"contrib.go.opencensus.io/exporter/ocagent",
"github.com/dgrijalva/jwt-go",
"github.com/dimchansky/utfbom",
"github.com/mitchellh/go-homedir",
"github.com/stretchr/testify/require",
"go.opencensus.io/plugin/ochttp",
"go.opencensus.io/plugin/ochttp/propagation/tracecontext",
"go.opencensus.io/stats/view",
"go.opencensus.io/trace",
"golang.org/x/crypto/pkcs12",
"golang.org/x/lint/golint",
]
solver-name = "gps-cdcl"
solver-version = 1

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