Compare commits

..

19 Commits

Author SHA1 Message Date
jillianwilson
3798d08f49 addressing pr comments 2021-11-11 19:13:16 -04:00
jillianwilson
5ad29d60e3 Addressing pr comments 2021-11-04 17:08:12 -03:00
jillianwilson
e9d2b63a58 Exiting out on certain startup errors 2021-11-04 17:04:15 -03:00
jillianwilson
0b4b9d117f Addressing PR comments 2021-11-04 16:58:17 -03:00
jillianwilson
1e1e3490c6 correcting README 2021-11-04 16:36:14 -03:00
Jillian W
af00cb27c7 Delete mutatingwebhook-ca-bundle.yaml 2021-11-01 13:31:16 -03:00
Jillian W
2433890cfd Delete golangci.yml 2021-11-01 13:30:40 -03:00
Jillian W
3542cafe88 Delete Makefile 2021-11-01 13:29:45 -03:00
jillianwilson
fe57e68769 Code cleanup and readme 2021-10-31 20:57:58 -03:00
jillianwilson
591b8949cd Revert "Add option to cosume connect events rather than polling to restart deployments"
This reverts commit a5f4a7a0c1.
2021-10-27 11:54:04 -03:00
jillianwilson
a8e6a4a4f1 Webhook that injects secrets into pods 2021-10-27 11:46:46 -03:00
jillianwilson
a5f4a7a0c1 Add option to cosume connect events rather than polling to restart deployments 2021-10-27 11:46:27 -03:00
Jillian W
b35c668959 Merge pull request #78 from 1Password/autorestart-webhook
Preparing Operator to handle Deployments that use the Kubernetes Webhook
2021-10-27 11:01:16 -03:00
jillianwilson
2fa035022c Adding better error handling when checking for op:// reference 2021-10-26 18:10:02 -03:00
jillianwilson
d715a6ed0e Addressing pr comments 2021-10-21 17:21:10 -03:00
jillianwilson
f439b04415 Adding supporting injected secrets via webhook 2021-10-15 12:20:45 -03:00
Jillian W
ee12dd449a Merge pull request #75 from 1Password/refactor-kubernetes
Moving operator code to a designated folder
2021-10-14 13:12:26 -03:00
jillianwilson
e6ca8d49ba Addressing PR comments 2021-10-06 14:07:37 -03:00
jillianwilson
f974d3f398 Moving operator code to a designated folder so that webhook work can also be included in this repo 2021-09-28 15:07:19 -03:00
2712 changed files with 891678 additions and 2921 deletions

View File

@@ -1 +1 @@
1.4.0
1.1.0

View File

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

87
.gitignore vendored
View File

@@ -1,25 +1,80 @@
# Temporary Build Files
build/_output
build/_test
# Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode
### Emacs ###
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/
dist/
# Flycheck
flycheck_*.el
# server auth directory
/server/
# projectiles files
.projectile
projectile-bookmarks.eld
# directory configuration
.dir-locals.el
# saveplace
places
# url cache
url/cache/
# cedet
ede-projects.el
# smex
smex-items
# company-statistics
company-statistics-cache.el
# anaconda-mode
anaconda-mode/
### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin
testbin/*
# Test binary, build with `go test -c`
# Test binary, build with 'go test -c'
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*
# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~
### Vim ###
# swap
.sw[a-p]
.*.sw[a-p]
# session
Session.vim
# temporary
.netrwhist
# auto-generated tag files
tags
### VisualStudioCode ###
.vscode/*
.history
.DS_Store
op-ss-client/
.idea/
# End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode

View File

@@ -12,124 +12,68 @@
---
[//]: # "START/v1.4.1"
# v1.4.1
## Fixes
- OwnerReferences on secrets are now persisted after an item is updated. {#101}
- Annotations from a Deployment or OnePasswordItem are no longer applied to Secrets that are created for it. {#102}
---
[//]: # "START/v1.4.0"
# v1.4.0
## Features
- The operator now declares the an OwnerReference for the secrets it manages. This should stop secrets from getting pruned by tools like Argo CD. {#51,#84,#96}
---
[//]: # "START/v1.3.0"
# v1.3.0
## Features
- Added support for loading secrets from files stored in 1Password. {#47}
---
[//]: # "START/v1.2.0"
# v1.2.0
## Features
- Support secrets provisioned through FromEnv. {#74}
- Support configuration of Kubernetes Secret type. {#87}
- Improved logging. (#72)
---
[//]: # "START/v1.1.0"
[//]: # (START/v1.1.0)
# v1.1.0
## Fixes
- Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
* Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
---
[//]: # "START/v1.0.2"
[//]: # (START/v1.0.2)
# v1.0.2
## Fixes
- Name normalizer added to handle non-conforming item names.
* Name normalizer added to handle non-conforming item names.
---
[//]: # "START/v1.0.1"
[//]: # (START/v1.0.1)
# v1.0.1
## Features
- This release also contains an arm64 Docker image. {#20}
- Docker images are also pushed to the :latest and :<major>.<minor> tags.
* This release also contains an arm64 Docker image. {#20}
* Docker images are also pushed to the :latest and :<major>.<minor> tags.
---
[//]: # "START/v1.0.0"
[//]: # (START/v1.0.0)
# v1.0.0
## Features:
- Option to automatically deploy 1Password Connect via the operator
- Ignore restart annotation when looking for 1Password annotations
- Release Automation
- Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource
- Adding configuration for auto rolling restart on deployments
- Configure Auto Restarts for a OnePasswordItem Custom Resource
- Update Connect Dependencies to latest
- Add Github action for building and testing operator
* Option to automatically deploy 1Password Connect via the operator
* Ignore restart annotation when looking for 1Password annotations
* Release Automation
* Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource
* Adding configuration for auto rolling restart on deployments
* Configure Auto Restarts for a OnePasswordItem Custom Resource
* Update Connect Dependencies to latest
* Add Github action for building and testing operator
## Fixes:
- Fix spec field example for OnePasswordItem in readme
- Casing of annotations are now consistent
* Fix spec field example for OnePasswordItem in readme
* Casing of annotations are now consistent
---
[//]: # "START/v0.0.2"
[//]: # (START/v0.0.2)
# v0.0.2
## Features:
- Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>`
* Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>`
---
[//]: # "START/v0.0.1"
[//]: # (START/v0.0.1)
# v0.0.1
Initial 1Password Operator release
## Features
- watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret
- watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets
- watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes
- restart pods when secret has been updated
- cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted
* watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret
* watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets
* watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes
* restart pods when secret has been updated
* cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted
---

View File

@@ -1,27 +0,0 @@
# Build the manager binary
FROM golang:1.17 as builder
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]

310
Makefile
View File

@@ -1,233 +1,83 @@
# VERSION defines the project version for the bundle.
# Update this value when you upgrade the version of your project.
# To re-generate a bundle for another specific version without changing the standard setup, you can:
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
VERSION ?= 0.0.1
export MAIN_BRANCH ?= main
# CHANNELS define the bundle channels used in the bundle.
# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
# To re-generate a bundle for other specific channels without changing the standard setup, you can:
# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable)
# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable")
ifneq ($(origin CHANNELS), undefined)
BUNDLE_CHANNELS := --channels=$(CHANNELS)
.DEFAULT_GOAL := help
.PHONY: test build build/binary build/local clean test/coverage release/prepare release/tag .check_bump_type .check_git_clean help
GIT_BRANCH := $(shell git symbolic-ref --short HEAD)
WORKTREE_CLEAN := $(shell git status --porcelain 1>/dev/null 2>&1; echo $$?)
SCRIPTS_DIR := $(CURDIR)/scripts
versionFile = $(CURDIR)/.VERSION
curVersion := $(shell cat $(versionFile) | sed 's/^v//')
OPERATOR_NAME := onepassword-connect-operator
INJECTOR_NAME := onepassword-secrets-injector
OPERATOR_DOCKER_IMG_TAG ?= $(OPERATOR_NAME):v$(curVersion)
INJECTOR_DOCKER_IMG_TAG ?= $(INJECTOR_NAME):v$(curVersion)
test: ## Run test suite
go test ./...
test/coverage: ## Run test suite with coverage report
go test -v ./... -cover
build/operator: ## Build operator Docker image
@docker build -f operator/Dockerfile --build-arg operator_version=$(curVersion) -t $(OPERATOR_DOCKER_IMG_TAG) .
@echo "Successfully built and tagged image."
@echo "Tag: $(OPERATOR_DOCKER_IMG_TAG)"
build/operator/local: ## Build local version of the operator Docker image
@docker build -f operator/Dockerfile -t local/$(OPERATOR_DOCKER_IMG_TAG) .
build/operator/binary: clean ## Build operator binary
@mkdir -p dist
@go build -mod vendor -a -o manager ./operator/cmd/manager/main.go
@mv manager ./dist
build/secret-injector: ## Build secret-injector Docker image
@docker build -f secret-injector/Dockerfile --build-arg operator_version=$(curVersion) -t $(INJECTOR_DOCKER_IMG_TAG) .
@echo "Successfully built and tagged image."
@echo "Tag: $(INJECTOR_DOCKER_IMG_TAG)"
build/secret-injector/local: ## Build local version of the secret-injector Docker image
@docker build -f secret-injector/Dockerfile -t local/$(INJECTOR_DOCKER_IMG_TAG) .
build/secret-injector/binary: clean ## Build secret-injector binary
@mkdir -p dist
@go build -mod vendor -a -o manager ./secret-injector/cmd/manager/main.go
@mv manager ./dist
clean:
rm -rf ./dist
help: ## Prints this help message
@grep -E '^[\/a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
## Release functions =====================
release/prepare: .check_git_clean ## Updates changelog and creates release branch (call with 'release/prepare version=<new_version_number>')
@test $(version) || (echo "[ERROR] version argument not set."; exit 1)
@git fetch --quiet origin $(MAIN_BRANCH)
@echo $(version) | tr -d '\n' | tee $(versionFile) &>/dev/null
@NEW_VERSION=$(version) $(SCRIPTS_DIR)/prepare-release.sh
release/tag: .check_git_clean ## Creates git tag
@git pull --ff-only
@echo "Applying tag 'v$(curVersion)' to HEAD..."
@git tag --sign "v$(curVersion)" -m "Release v$(curVersion)"
@echo "[OK] Success!"
@echo "Remember to call 'git push --tags' to persist the tag."
## Helper functions =====================
.check_git_clean:
ifneq ($(GIT_BRANCH), $(MAIN_BRANCH))
@echo "[ERROR] Please checkout default branch '$(MAIN_BRANCH)' and re-run this command."; exit 1;
endif
# DEFAULT_CHANNEL defines the default channel used in the bundle.
# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable")
# To re-generate a bundle for any other default channel without changing the default setup, you can:
# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable)
# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable")
ifneq ($(origin DEFAULT_CHANNEL), undefined)
BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
ifneq ($(WORKTREE_CLEAN), 0)
@echo "[ERROR] Uncommitted changes found in worktree. Address them and try again."; exit 1;
endif
BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images.
# This variable is used to construct full image tags for bundle and catalog images.
#
# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
# onepassword.com/onepassword-operator-new-bundle:$VERSION and onepassword.com/onepassword-operator-new-catalog:$VERSION.
IMAGE_TAG_BASE ?= onepassword.com/onepassword-operator-new
# BUNDLE_IMG defines the image:tag used for the bundle.
# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)
# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command
BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests
# You can enable this value if you would like to use SHA Based Digests
# To enable set flag to true
USE_IMAGE_DIGESTS ?= false
ifeq ($(USE_IMAGE_DIGESTS), true)
BUNDLE_GEN_FLAGS += --use-image-digests
endif
# Image URL to use all building/pushing image targets
IMG ?= controller:latest
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.23
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
# Setting SHELL to bash allows bash commands to be executed by recipes.
# This is a requirement for 'setup-envtest.sh' in the test target.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec
.PHONY: all
all: build
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development
.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
.PHONY: fmt
fmt: ## Run go fmt against code.
go fmt ./...
.PHONY: vet
vet: ## Run go vet against code.
go vet ./...
.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -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
.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}
##@ 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: deploy
deploy: manifests kustomize ## 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 -
CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
.PHONY: controller-gen
controller-gen: ## Download controller-gen locally if necessary.
$(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0)
KUSTOMIZE = $(shell pwd)/bin/kustomize
.PHONY: kustomize
kustomize: ## Download kustomize locally if necessary.
$(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7)
ENVTEST = $(shell pwd)/bin/setup-envtest
.PHONY: envtest
envtest: ## Download envtest-setup locally if necessary.
$(call go-get-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)
# go-get-tool will 'go get' any package $2 and install it to $1.
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
define go-get-tool
@[ -f $(1) ] || { \
set -e ;\
TMP_DIR=$$(mktemp -d) ;\
cd $$TMP_DIR ;\
go mod init tmp ;\
echo "Downloading $(2)" ;\
GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\
rm -rf $$TMP_DIR ;\
}
endef
.PHONY: 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.19.1/$${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)

19
PROJECT
View File

@@ -1,19 +0,0 @@
domain: onepassword.com
layout:
- go.kubebuilder.io/v3
plugins:
manifests.sdk.operatorframework.io/v2: {}
scorecard.sdk.operatorframework.io/v2: {}
projectName: onepassword-operator-new
repo: github.com/1Password/onepassword-operator
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: onepassword.com
group: onepassword
kind: OnePasswordItem
path: github.com/1Password/onepassword-operator/api/v1
version: v1
version: "3"

231
README.md
View File

@@ -1,6 +1,8 @@
// TODO: Update README.md
# 1Password for Kubernetes
# 1Password Connect Kubernetes Operator
This repository includes various tooling for integrating 1Password secrets wtih Kubernetes.
## 1Password Connect Kubernetes Operator
The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item.
@@ -8,231 +10,16 @@ The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to
The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted.
## Setup
[Click here for more details on the 1Password Kubernetes Operator](operator/README.md)
Prerequisites:
## 1Password Secrets Injector for Kubernetes
- [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/)
- [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [docker installed](https://docs.docker.com/get-docker/)
- [Generated a 1password-credentials.json file and issued a 1Password Connect API Token for the K8s Operator integration](https://support.1password.com/secrets-automation/)
- [1Password Connect deployed to Kubernetes](https://support.1password.com/connect-deploy-kubernetes/#step-2-deploy-a-1password-connect-server). **NOTE**: If customization of the 1Password Connect deployment is not required you can skip this prerequisite.
The 1Password Secrets Injector implements a mutating webhook to inject 1Password secrets as environment variables into a pod or deployment. Unlike the 1Password Kubernetes Operator, the Secrets Injector does not create a Kubernetes Secret when assigning secrets to your resource.
### Quickstart for Deploying 1Password Connect to Kubernetes
[Click here for more details on the 1Password Secrets Injector for Kubernetes](secret-injector/README.md)
#### Deploy with Helm
The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes.
[The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts)
#### Deploy using the Connect Operator
If 1Password Connect is already running, you can skip this step. This guide will provide a quickstart option for deploying a default configuration of 1Password Connect via starting the deploying the 1Password Connect Operator, however it is recommended that you instead deploy your own manifest file if customization of the 1Password Connect deployment is desired.
Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session:
```bash
cat 1password-credentials.json | base64 | \
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
```
Create a Kubernetes secret from the op-session file:
```bash
kubectl create secret generic op-credentials --from-file=op-session
```
Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`:
```yaml
- name: MANAGE_CONNECT
value: "true"
```
Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the `default` namespace.
### Kubernetes Operator Deployment
**Create Kubernetes Secret for OP_CONNECT_TOKEN**
"Create a Connect token for the operator and save it as a Kubernetes Secret:
```bash
kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>"
```
If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command:
```bash
kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
```
[More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens)
**Set Permissions For Operator**
We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder.
```bash
kubectl apply -f deploy/permissions.yaml
```
**Create Custom One Password Secret Resource**
```bash
kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
```
**Deploying the Operator**
An sample Deployment yaml can be found at `/deploy/operator.yaml`.
To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml:
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect.
- **WATCH_NAMESPACE:** (default: watch all namespaces): Comma separated list of what Namespaces to watch for changes.
- **POLLING_INTERVAL** (default: 600): The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
- **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
Apply the deployment file:
```yaml
kubectl apply -f deploy/operator.yaml
```
## Usage
To create a Kubernetes Secret from a 1Password item, create a yaml file with the following
```yaml
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: <item_name> #this name will also be used for naming the generated kubernetes secret
spec:
itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
```
Deploy the OnePasswordItem to Kubernetes:
```bash
kubectl apply -f <your_item>.yaml
```
To test that the Kubernetes Secret check that the following command returns a secret:
```bash
kubectl get secret <secret_name>
```
Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.
To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-example
annotations:
operator.1password.io/item-path: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
operator.1password.io/item-name: "<secret_name>"
```
Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Path.
The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password.
In case of fields that store files, the file's contents will be used as the value.
Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence.
Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret.
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
---
**NOTE**
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.
Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format:
- Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed
- All whitespaces between words will be replaced by `-`
- All the letters will be lower-cased.
---
### Configuring Automatic Rolling Restarts of Deployments
If a 1Password Item that is linked to a Kubernetes Secret is updated, any deployments configured to `auto-restart` AND are using that secret will be given a rolling restart the next time 1Password Connect is polled for updates.
There are many levels of granularity on which to configure auto restarts on deployments: at the operator level, per-namespace, or per-deployment.
**On the operator**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environemnt variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false.
**Per Namespace**: This method allows for managing auto restarts on all deployments within a namespace. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired namespace. An example of this is shown below:
```yaml
# enabled auto restarts for all deployments within a namespace unless overwritten within a deployment
apiVersion: v1
kind: Namespace
metadata:
name: "example-namespace"
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment.
**Per Deployment**
This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below:
```yaml
# enabled auto restarts for the deployment
apiVersion: v1
kind: Deployment
metadata:
name: "example-deployment"
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the namespace will be used.
**Per OnePasswordItem Custom Resource**
This method allows for managing auto restarts on a given OnePasswordItem custom resource. Auto restarts can by managed by setting the annotation `operator.1password.io/auto_restart` to either `true` or `false` on the desired OnePasswordItem. An example of this is shown below:
```yaml
# enabled auto restarts for the OnePasswordItem
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: example
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the deployment will be used.
## Development
### Creating a Docker image
To create a local version of the Docker image for testing, use the following `Makefile` target:
```shell
make build/local
```
### Building the Operator binary
```shell
make build/binary
```
The binary will be placed inside a `dist` folder within this repository.
### Running Tests
```shell
make test
```
With coverage:
```shell
make test/coverage
```
## Security
# Security
1Password requests you practice responsible disclosure if you discover a vulnerability.

View File

@@ -1,36 +0,0 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1 contains API Schema definitions for the onepassword v1 API group
//+kubebuilder:object:generate=true
//+groupName=onepassword.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.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,21 +0,0 @@
# 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.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

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

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

View File

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

View File

@@ -1,74 +0,0 @@
# Adds namespace to all resources.
namespace: onepassword-operator-new-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: onepassword-operator-new-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../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
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
# fieldref:
# fieldpath: metadata.namespace
#- name: CERTIFICATE_NAME
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
#- name: SERVICE_NAMESPACE # namespace of the service
# objref:
# kind: Service
# version: v1
# name: webhook-service
# fieldref:
# fieldpath: metadata.namespace
#- name: SERVICE_NAME
# objref:
# kind: Service
# version: v1
# name: webhook-service

View File

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

View File

@@ -1,20 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
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

@@ -1,11 +0,0 @@
apiVersion: controller-runtime.sigs.k8s.io/v1alpha1
kind: ControllerManagerConfig
health:
healthProbeBindAddress: :8081
metrics:
bindAddress: 127.0.0.1:8080
webhook:
port: 9443
leaderElection:
leaderElect: true
resourceName: c26807fd.onepassword.com

View File

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

View File

@@ -1,60 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
labels:
control-plane: controller-manager
name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
labels:
control-plane: controller-manager
spec:
selector:
matchLabels:
control-plane: controller-manager
replicas: 1
template:
metadata:
annotations:
kubectl.kubernetes.io/default-container: manager
labels:
control-plane: controller-manager
spec:
securityContext:
runAsNonRoot: true
containers:
- command:
- /manager
args:
- --leader-elect
image: controller:latest
name: manager
securityContext:
allowPrivilegeEscalation: false
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: controller-manager
terminationGracePeriodSeconds: 10

View File

@@ -1,27 +0,0 @@
# These resources constitute the fully configured set of manifests
# used to generate the 'manifests/' directory in a bundle.
resources:
- bases/onepassword-operator-new.clusterserviceversion.yaml
- ../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/1/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

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

View File

@@ -1,20 +0,0 @@
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
control-plane: controller-manager
name: controller-manager-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:
control-plane: controller-manager

View File

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

View File

@@ -1,17 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: proxy-role
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create

View File

@@ -1,12 +0,0 @@
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: controller-manager
namespace: system

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
labels:
control-plane: controller-manager
name: controller-manager-metrics-service
namespace: system
spec:
ports:
- name: https
port: 8443
protocol: TCP
targetPort: https
selector:
control-plane: controller-manager

View File

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

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

@@ -1,12 +0,0 @@
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: controller-manager
namespace: system

View File

@@ -1,24 +0,0 @@
# permissions for end users to edit onepassworditems.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: onepassworditem-editor-role
rules:
- apiGroups:
- onepassword.onepassword.com
resources:
- onepassworditems
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- onepassword.onepassword.com
resources:
- onepassworditems/status
verbs:
- get

View File

@@ -1,20 +0,0 @@
# permissions for end users to view onepassworditems.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: onepassworditem-viewer-role
rules:
- apiGroups:
- onepassword.onepassword.com
resources:
- onepassworditems
verbs:
- get
- list
- watch
- apiGroups:
- onepassword.onepassword.com
resources:
- onepassworditems/status
verbs:
- get

View File

@@ -1,12 +0,0 @@
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: controller-manager
namespace: system

View File

@@ -1,5 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: controller-manager
namespace: system

View File

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

View File

@@ -1,6 +0,0 @@
apiVersion: onepassword.onepassword.com/v1
kind: OnePasswordItem
metadata:
name: onepassworditem-sample
spec:
# TODO(user): Add fields here

View File

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

View File

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

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

View File

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

View File

@@ -1,183 +0,0 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"context"
"fmt"
"github.com/1Password/onepassword-operator/pkg/onepassword"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/source"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/connect-sdk-go/connect"
corev1 "k8s.io/api/core/v1"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var log = logf.Log.WithName("controller_onepassworditem")
// OnePasswordItemReconciler reconciles a OnePasswordItem object
type OnePasswordItemReconciler struct {
Client kubeClient.Client
Scheme *runtime.Scheme
OpConnectClient connect.Client
}
//+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the OnePasswordItem object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile
func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling OnePasswordItem")
onepassworditem := &onepasswordv1.OnePasswordItem{}
err := r.Client.Get(context.Background(), request.NamespacedName, onepassworditem)
if err != nil {
if errors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
// If the deployment is not being deleted
if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() {
// Adds a finalizer to the deployment if one does not exist.
// This is so we can handle cleanup of associated secrets properly
if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer)
if err := r.Client.Update(context.Background(), onepassworditem); err != nil {
return reconcile.Result{}, err
}
}
// Handles creation or updating secrets for deployment if needed
if err := r.HandleOnePasswordItem(onepassworditem, request); err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
// If one password finalizer exists then we must cleanup associated secrets
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
// Delete associated kubernetes secret
if err = r.cleanupKubernetesSecret(onepassworditem); err != nil {
return reconcile.Result{}, err
}
// Remove finalizer now that cleanup is complete
if err := r.removeFinalizer(onepassworditem); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error {
c, err := controller.New("onepassworditem-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to primary resource OnePasswordItem
err = c.Watch(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
return nil
// TODO Consider the simplified code below. Based on the migration guide: https://sdk.operatorframework.io/docs/building-operators/golang/migration/#create-a-new-project
// return ctrl.NewControllerManagedBy(mgr).Named("onepassworditem-controller").WithOptions(controller.Options{Reconciler: r}).
// For(&onepasswordv1.OnePasswordItem{}).Watches(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{}).
// Complete(r)
}
func (r *OnePasswordItemReconciler) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error {
onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer)
if err := r.Client.Update(context.Background(), onePasswordItem); err != nil {
return err
}
return nil
}
func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error {
kubernetesSecret := &corev1.Secret{}
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace
r.Client.Delete(context.Background(), kubernetesSecret)
if err := r.Client.Delete(context.Background(), kubernetesSecret); err != nil {
if !errors.IsNotFound(err) {
return err
}
}
return nil
}
func (r *OnePasswordItemReconciler) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
secretName := resource.GetName()
labels := resource.Labels
annotations := resource.Annotations
secretType := resource.Type
autoRestart := annotations[op.RestartDeploymentsAnnotation]
item, err := onepassword.GetOnePasswordItemByPath(r.OpConnectClient, resource.Spec.ItemPath)
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}
// Create owner reference.
gvk, err := apiutil.GVKForObject(resource, r.Scheme)
if err != nil {
return fmt.Errorf("could not to retrieve group version kind: %v", err)
}
ownerRef := &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: resource.GetName(),
UID: resource.GetUID(),
}
return kubeSecrets.CreateKubernetesSecretFromItem(r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, annotations, ownerRef)
}

View File

@@ -1,80 +0,0 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"path/filepath"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
//+kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
err = onepasswordv1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
//+kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
}, 60)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})

89
go.mod
View File

@@ -1,81 +1,22 @@
module github.com/1Password/onepassword-operator
go 1.17
go 1.13
require (
github.com/1Password/connect-sdk-go v1.2.0
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.17.0
github.com/stretchr/testify v1.7.0
k8s.io/api v0.23.5
k8s.io/apimachinery v0.23.5
k8s.io/client-go v0.23.5
k8s.io/kubectl v0.23.5
sigs.k8s.io/controller-runtime v0.11.0
github.com/1Password/connect-sdk-go v1.0.1
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/operator-framework/operator-sdk v0.19.0
github.com/prometheus/common v0.14.0 // indirect
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.6.1
k8s.io/api v0.18.2
k8s.io/apimachinery v0.18.2
k8s.io/client-go v12.0.0+incompatible
k8s.io/kubectl v0.18.2
sigs.k8s.io/controller-runtime v0.6.0
)
require (
cloud.google.com/go v0.81.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.18 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/go-logr/zapr v1.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.28.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.0+incompatible // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiextensions-apiserver v0.23.0 // indirect
k8s.io/component-base v0.23.5 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
replace (
github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM
k8s.io/client-go => k8s.io/client-go v0.18.2 // Required by prometheus-operator
)

1475
go.sum

File diff suppressed because it is too large Load Diff

254
main.go
View File

@@ -1,254 +0,0 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"errors"
"flag"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/1Password/connect-sdk-go/connect"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
"github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/onepassword-operator/version"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
// sdkVersion "github.com/operator-framework/operator-sdk/version"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
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/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/controllers"
//+kubebuilder:scaffold:imports
)
var (
scheme = k8sruntime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
WatchNamespaceEnvVar = "WATCH_NAMESPACE"
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(onepasswordv1.AddToScheme(scheme))
//+kubebuilder:scaffold:scheme
}
func printVersion() {
setupLog.Info(fmt.Sprintf("Operator Version: %s", version.Version))
setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
// TODO figure out how to get operator-sdk version
// setupLog.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version))
}
func main() {
var metricsAddr string
var enableLeaderElection bool
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()
namespace := os.Getenv(WatchNamespaceEnvVar)
options := ctrl.Options{
Scheme: scheme,
Namespace: namespace,
MetricsBindAddress: metricsAddr,
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "c26807fd.onepassword.com",
}
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
// Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate
// Also note that you may face performance issues when using this with a high number of namespaces.
if strings.Contains(namespace, ",") {
options.Namespace = ""
options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(namespace, ","))
}
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, "failed to create 1Password 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)
}
//+kubebuilder:scaffold:builder
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)
}
deploymentNamespace, err := utils.GetOperatorNamespace()
if err != nil {
setupLog.Error(err, "Failed to get namespace")
os.Exit(1)
}
//Setup 1PasswordConnect
if shouldManageConnect() {
setupLog.Info("Automated Connect Management Enabled")
go func() {
connectStarted := false
for !connectStarted {
err := op.SetupConnect(mgr.GetClient(), deploymentNamespace)
// Cache Not Started is an acceptable error. Retry until cache is started.
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
setupLog.Error(err, "")
os.Exit(1)
}
if err == nil {
connectStarted = true
}
}
}()
} else {
setupLog.Info("Automated Connect Management Disabled")
}
// TODO: Configure Metrics Service. See: https://sdk.operatorframework.io/docs/building-operators/golang/migration/#export-metrics
// Setup update secrets task
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments())
done := make(chan bool)
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
go func() {
for {
select {
case <-done:
ticker.Stop()
return
case <-ticker.C:
err := updatedSecretsPoller.UpdateKubernetesSecretsTask()
if err != nil {
setupLog.Error(err, "error running update kubernetes secret task")
}
}
}
}()
// Start the Cmd
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
setupLog.Error(err, "Manager exited non-zero")
done <- true
os.Exit(1)
}
}
const manageConnect = "MANAGE_CONNECT"
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
}
const envPollingIntervalVariable = "POLLING_INTERVAL"
const defaultPollingInterval = 600
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
}
const restartDeploymentsEnvVariable = "AUTO_RESTART"
func shouldAutoRestartDeployments() bool {
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
if found {
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
if err != nil {
setupLog.Error(err, "")
os.Exit(1)
}
return shouldAutoRestartDeploymentsBool
}
return false
}

31
operator/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
# Build the manager binary
FROM golang:1.13 as builder
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# Copy the go source
COPY operator/cmd/manager/main.go operator/main.go
COPY operator/pkg/ operator/pkg/
COPY operator/version/ operator/version/
COPY vendor/ vendor/
# Build
ARG operator_version=dev
RUN CGO_ENABLED=0 \
GO111MODULE=on \
go build \
-ldflags "-X \"github.com/1Password/onepassword-operator/operator/version.Version=$operator_version\"" \
-mod vendor \
-a -o manager operator/main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER nonroot:nonroot
COPY operator/deploy/connect/ operator/deploy/connect/
ENTRYPOINT ["/manager"]

235
operator/README.md Normal file
View File

@@ -0,0 +1,235 @@
# 1Password Connect Kubernetes Operator
The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item.
The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment.
The 1Password Connect Kubernetes Operator 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:
- [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/)
- [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [docker installed](https://docs.docker.com/get-docker/)
- [Generated a 1password-credentials.json file and issued a 1Password Connect API Token for the K8s Operator integration](https://support.1password.com/secrets-automation/)
- [1Password Connect deployed to Kubernetes](https://support.1password.com/connect-deploy-kubernetes/#step-2-deploy-a-1password-connect-server). **NOTE**: If customization of the 1Password Connect deployment is not required you can skip this prerequisite.
### Quickstart for Deploying 1Password Connect to Kubernetes
#### Deploy with Helm
The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes.
[The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts)
#### Deploy using the Connect Operator
If 1Password Connect is already running, you can skip this step. This guide will provide a quickstart option for deploying a default configuration of 1Password Connect via starting the deploying the 1Password Connect Operator, however it is recommended that you instead deploy your own manifest file if customization of the 1Password Connect deployment is desired.
Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session:
```bash
$ cat 1password-credentials.json | base64 | \
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
```
Create a Kubernetes secret from the op-session file:
```bash
$ kubectl create secret generic op-credentials --from-file=1password-credentials.json
```
Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`:
```yaml
- name: MANAGE_CONNECT
value: "true"
```
Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the `default` namespace.
### Kubernetes Operator Deployment
**Create Kubernetes Secret for OP_CONNECT_TOKEN**
"Create a Connect token for the operator and save it as a Kubernetes Secret:
```bash
$ kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>"
```
If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command:
```bash
$ kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
```
[More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens)
**Set Permissions For Operator**
We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder.
```bash
$ kubectl apply -f deploy/permissions.yaml
```
**Create Custom One Password Secret Resource**
```bash
$ kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
```
**Deploying the Operator**
An sample Deployment yaml can be found at `/deploy/operator.yaml`.
To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml:
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect.
- **WATCH_NAMESPACE:** (default: watch all namespaces): Comma separated list of what Namespaces to watch for changes.
- **POLLING_INTERVAL** (default: 600): The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
- **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
Apply the deployment file:
```yaml
kubectl apply -f deploy/operator.yaml
```
## Usage
To create a Kubernetes Secret from a 1Password item, create a yaml file with the following
```yaml
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: <item_name> #this name will also be used for naming the generated kubernetes secret
spec:
itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
```
Deploy the OnePasswordItem to Kubernetes:
```bash
$ kubectl apply -f <your_item>.yaml
```
To test that the Kubernetes Secret check that the following command returns a secret:
```bash
$ kubectl get secret <secret_name>
```
Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.
To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-example
annotations:
operator.1password.io/item-path: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
operator.1password.io/item-name: "<secret_name>"
```
Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Path.
Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret.
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
---
**NOTE**
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.
Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format:
- Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed
- All whitespaces between words will be replaced by `-`
- All the letters will be lower-cased.
---
### Configuring Automatic Rolling Restarts of Deployments
If a 1Password Item that is linked to a Kubernetes Secret is updated, any deployments configured to `auto-restart` AND are using that secret will be given a rolling restart the next time 1Password Connect is polled for updates.
There are many levels of granularity on which to configure auto restarts on deployments: at the operator level, per-namespace, or per-deployment.
**On the operator**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environemnt variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false.
**Per Namespace**: This method allows for managing auto restarts on all deployments within a namespace. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired namespace. An example of this is shown below:
```yaml
# enabled auto restarts for all deployments within a namespace unless overwritten within a deployment
apiVersion: v1
kind: Namespace
metadata:
name: "example-namespace"
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto reset settings on the operator will be used. This value can be overwritten by deployment.
**Per Deployment**
This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below:
```yaml
# enabled auto restarts for the deployment
apiVersion: v1
kind: Deployment
metadata:
name: "example-deployment"
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto reset settings on the namespace will be used.
**Per OnePasswordItem Custom Resource**
This method allows for managing auto restarts on a given OnePasswordItem custom resource. Auto restarts can by managed by setting the annotation `operator.1password.io/auto_restart` to either `true` or `false` on the desired OnePasswordItem. An example of this is shown below:
```yaml
# enabled auto restarts for the OnePasswordItem
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: example
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto reset settings on the deployment will be used.
## Development
### Creating a Docker image
To create a local version of the Docker image for testing, use the following `Makefile` target:
```shell
make build/local
```
### Building the Operator binary
```shell
make build/binary
```
The binary will be placed inside a `dist` folder within this repository.
### Running Tests
```shell
make test
```
With coverage:
```shell
make test/coverage
```
## Security
1Password requests you practice responsible disclosure if you discover a vulnerability.
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits).

View File

@@ -0,0 +1,302 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/1Password/onepassword-operator/operator/pkg/controller"
op "github.com/1Password/onepassword-operator/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/operator/pkg/apis"
"github.com/1Password/onepassword-operator/operator/version"
"github.com/1Password/connect-sdk-go/connect"
"github.com/operator-framework/operator-sdk/pkg/k8sutil"
kubemetrics "github.com/operator-framework/operator-sdk/pkg/kube-metrics"
"github.com/operator-framework/operator-sdk/pkg/leader"
"github.com/operator-framework/operator-sdk/pkg/log/zap"
"github.com/operator-framework/operator-sdk/pkg/metrics"
sdkVersion "github.com/operator-framework/operator-sdk/version"
"github.com/spf13/pflag"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client/config"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
)
const envPollingIntervalVariable = "POLLING_INTERVAL"
const manageConnect = "MANAGE_CONNECT"
const restartDeploymentsEnvVariable = "AUTO_RESTART"
const defaultPollingInterval = 600
// Change below variables to serve metrics on different host or port.
var (
metricsHost = "0.0.0.0"
metricsPort int32 = 8383
operatorMetricsPort int32 = 8686
)
var log = logf.Log.WithName("cmd")
func printVersion() {
log.Info(fmt.Sprintf("Operator Version: %s", version.Version))
log.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
log.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version))
}
func main() {
// Add the zap logger flag set to the CLI. The flag set must
// be added before calling pflag.Parse().
pflag.CommandLine.AddFlagSet(zap.FlagSet())
// Add flags registered by imported packages (e.g. glog and
// controller-runtime)
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
// Use a zap logr.Logger implementation. If none of the zap
// flags are configured (or if the zap flag set is not being
// used), this defaults to a production zap logger.
//
// The logger instantiated here can be changed to any logger
// implementing the logr.Logger interface. This logger will
// be propagated through the whole operator, generating
// uniform and structured logs.
logf.SetLogger(zap.Logger())
printVersion()
namespace := os.Getenv(k8sutil.WatchNamespaceEnvVar)
deploymentNamespace, err := k8sutil.GetOperatorNamespace()
if err != nil {
log.Error(err, "Failed to get namespace")
os.Exit(1)
}
// Get a config to talk to the apiserver
cfg, err := config.GetConfig()
if err != nil {
log.Error(err, "")
os.Exit(1)
}
ctx := context.Background()
// Become the leader before proceeding
err = leader.Become(ctx, "onepassword-connect-operator-lock")
if err != nil {
log.Error(err, "")
os.Exit(1)
}
// Set default manager options
options := manager.Options{
Namespace: namespace,
MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort),
}
// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
// Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate
// Also note that you may face performance issues when using this with a high number of namespaces.
if strings.Contains(namespace, ",") {
options.Namespace = ""
options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(namespace, ","))
}
// Create a new manager to provide shared dependencies and start components
mgr, err := manager.New(cfg, options)
if err != nil {
log.Error(err, "")
os.Exit(1)
}
log.Info("Registering Components.")
// Setup Scheme for all resources
if err := apis.AddToScheme(mgr.GetScheme()); err != nil {
log.Error(err, "")
os.Exit(1)
}
//Setup 1PasswordConnect
if shouldManageConnect() {
log.Info("Automated Connect Management Enabled")
go func() {
connectStarted := false
for connectStarted == false {
err := op.SetupConnect(mgr.GetClient(), deploymentNamespace)
// Cache Not Started is an acceptable error. Retry until cache is started.
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
log.Error(err, "")
os.Exit(1)
}
if err == nil {
connectStarted = true
}
}
}()
} else {
log.Info("Automated Connect Management Disabled")
}
// Setup One Password Client
opConnectClient, err := connect.NewClientFromEnvironment()
if err := controller.AddToManager(mgr, opConnectClient); err != nil {
log.Error(err, "")
os.Exit(1)
}
// Add the Metrics Service
addMetrics(ctx, cfg)
// Setup update secrets task
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments())
done := make(chan bool)
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
go func() {
for {
select {
case <-done:
ticker.Stop()
return
case <-ticker.C:
updatedSecretsPoller.UpdateKubernetesSecretsTask()
}
}
}()
// Start the Cmd
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
log.Error(err, "Manager exited non-zero")
done <- true
os.Exit(1)
}
}
// addMetrics will create the Services and Service Monitors to allow the operator export the metrics by using
// the Prometheus operator
func addMetrics(ctx context.Context, cfg *rest.Config) {
// Get the namespace the operator is currently deployed in.
operatorNs, err := k8sutil.GetOperatorNamespace()
if err != nil {
if errors.Is(err, k8sutil.ErrRunLocal) {
log.Info("Skipping CR metrics server creation; not running in a cluster.")
return
}
}
if err := serveCRMetrics(cfg, operatorNs); err != nil {
log.Info("Could not generate and serve custom resource metrics", "error", err.Error())
}
// Add to the below struct any other metrics ports you want to expose.
servicePorts := []v1.ServicePort{
{Port: metricsPort, Name: metrics.OperatorPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: metricsPort}},
{Port: operatorMetricsPort, Name: metrics.CRPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: operatorMetricsPort}},
}
// Create Service object to expose the metrics port(s).
service, err := metrics.CreateMetricsService(ctx, cfg, servicePorts)
if err != nil {
log.Info("Could not create metrics Service", "error", err.Error())
}
// CreateServiceMonitors will automatically create the prometheus-operator ServiceMonitor resources
// necessary to configure Prometheus to scrape metrics from this operator.
services := []*v1.Service{service}
// The ServiceMonitor is created in the same namespace where the operator is deployed
_, err = metrics.CreateServiceMonitors(cfg, operatorNs, services)
if err != nil {
log.Info("Could not create ServiceMonitor object", "error", err.Error())
// If this operator is deployed to a cluster without the prometheus-operator running, it will return
// ErrServiceMonitorNotPresent, which can be used to safely skip ServiceMonitor creation.
if err == metrics.ErrServiceMonitorNotPresent {
log.Info("Install prometheus-operator in your cluster to create ServiceMonitor objects", "error", err.Error())
}
}
}
// serveCRMetrics gets the Operator/CustomResource GVKs and generates metrics based on those types.
// It serves those metrics on "http://metricsHost:operatorMetricsPort".
func serveCRMetrics(cfg *rest.Config, operatorNs string) error {
// The function below returns a list of filtered operator/CR specific GVKs. For more control, override the GVK list below
// with your own custom logic. Note that if you are adding third party API schemas, probably you will need to
// customize this implementation to avoid permissions issues.
filteredGVK, err := k8sutil.GetGVKsFromAddToScheme(apis.AddToScheme)
if err != nil {
return err
}
// The metrics will be generated from the namespaces which are returned here.
// NOTE that passing nil or an empty list of namespaces in GenerateAndServeCRMetrics will result in an error.
ns, err := kubemetrics.GetNamespacesForMetrics(operatorNs)
if err != nil {
return err
}
// Generate and serve custom resource specific metrics.
err = kubemetrics.GenerateAndServeCRMetrics(cfg, ns, filteredGVK, metricsHost, operatorMetricsPort)
if err != nil {
return err
}
return nil
}
func getPollingIntervalForUpdatingSecrets() time.Duration {
timeInSecondsString, found := os.LookupEnv(envPollingIntervalVariable)
if found {
timeInSeconds, err := strconv.Atoi(timeInSecondsString)
if err == nil {
return time.Duration(timeInSeconds) * time.Second
}
log.Info("Invalid value set for polling interval. Must be a valid integer.")
}
log.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval))
return time.Duration(defaultPollingInterval) * time.Second
}
func shouldManageConnect() bool {
shouldManageConnect, found := os.LookupEnv(manageConnect)
if found {
shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect))
if err != nil {
log.Error(err, "")
os.Exit(1)
}
return shouldManageConnectBool
}
return false
}
func shouldAutoRestartDeployments() bool {
shouldAutoRestartDeployments, found := os.LookupEnv(restartDeploymentsEnvVariable)
if found {
shouldAutoRestartDeploymentsBool, err := strconv.ParseBool(strings.ToLower(shouldAutoRestartDeployments))
if err != nil {
log.Error(err, "")
os.Exit(1)
}
return shouldAutoRestartDeploymentsBool
}
return false
}

View File

@@ -39,7 +39,4 @@ spec:
status:
description: OnePasswordItemStatus defines the observed state of OnePasswordItem
type: object
type:
description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
type: string
type: object

View File

@@ -0,0 +1,10 @@
package apis
import (
v1 "github.com/1Password/onepassword-operator/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)
}

13
operator/pkg/apis/apis.go Normal file
View File

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

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

@@ -0,0 +1,4 @@
// 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,57 +1,37 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// OnePasswordItemSpec defines the desired state of OnePasswordItem
type OnePasswordItemSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Foo is an example field of OnePasswordItem. Edit onepassworditem_types.go to remove/update
ItemPath string `json:"itemPath,omitempty"`
}
// OnePasswordItemStatus defines the observed state of OnePasswordItem
type OnePasswordItemStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
// 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
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// OnePasswordItem is the Schema for the onepassworditems API
// +kubebuilder:subresource:status
// +kubebuilder:resource:path=onepassworditems,scope=Namespaced
type OnePasswordItem struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Type string `json:"type,omitempty"`
Spec OnePasswordItemSpec `json:"spec,omitempty"`
Status OnePasswordItemStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// OnePasswordItemList contains a list of OnePasswordItem
type OnePasswordItemList struct {

View File

@@ -0,0 +1,19 @@
// 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,23 +1,6 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by controller-gen. DO NOT EDIT.
// Code generated by operator-sdk. DO NOT EDIT.
package v1
@@ -32,6 +15,7 @@ func (in *OnePasswordItem) DeepCopyInto(out *OnePasswordItem) {
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
out.Status = in.Status
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItem.
@@ -64,6 +48,7 @@ func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemList.
@@ -87,6 +72,7 @@ func (in *OnePasswordItemList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OnePasswordItemSpec) DeepCopyInto(out *OnePasswordItemSpec) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemSpec.
@@ -102,6 +88,7 @@ func (in *OnePasswordItemSpec) DeepCopy() *OnePasswordItemSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemStatus.

View File

@@ -0,0 +1,10 @@
package controller
import (
"github.com/1Password/onepassword-operator/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

@@ -0,0 +1,10 @@
package controller
import (
"github.com/1Password/onepassword-operator/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

@@ -0,0 +1,19 @@
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,12 +1,14 @@
package controllers
package deployment
import (
"context"
"fmt"
"strings"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
"github.com/1Password/onepassword-operator/pkg/utils"
kubeSecrets "github.com/1Password/onepassword-operator/operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/operator/pkg/onepassword"
op "github.com/1Password/onepassword-operator/operator/pkg/onepassword"
"github.com/1Password/onepassword-operator/operator/pkg/utils"
"regexp"
@@ -14,11 +16,9 @@ import (
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"
@@ -27,7 +27,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/source"
)
var deploymentLog = logf.Log.WithName("controller_deployment")
var log = logf.Log.WithName("controller_deployment")
var finalizer = "onepassword.com/finalizer.secret"
const annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
@@ -71,23 +71,9 @@ type ReconcileDeployment struct {
}
func (r *ReconcileDeployment) SetupWithManager(mgr ctrl.Manager) error {
c, err := controller.New("deployment-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to primary resource Deployment
err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
return nil
// TODO figure out what to do with this code.
// return ctrl.NewControllerManagedBy(mgr).
// For(&appsv1.Deployment{}).
// Complete(r)
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1.Deployment{}).
Complete(r)
}
func (r *ReconcileDeployment) test() {
@@ -99,8 +85,9 @@ func (r *ReconcileDeployment) test() {
// Note:
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (r *ReconcileDeployment) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
reqLogger := deploymentLog.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
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{}
@@ -129,7 +116,7 @@ func (r *ReconcileDeployment) Reconcile(ctx context.Context, request reconcile.R
}
}
// Handles creation or updating secrets for deployment if needed
if err := r.HandleApplyingDeployment(deployment, deployment.Namespace, annotations, request); err != nil {
if err := r.HandleApplyingDeployment(deployment, annotations, request); err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
@@ -183,7 +170,7 @@ func (r *ReconcileDeployment) areMultipleDeploymentsUsingSecret(updatedSecrets m
err := r.kubeClient.List(context.Background(), deployments, opts...)
if err != nil {
deploymentLog.Error(err, "Failed to list kubernetes deployments")
log.Error(err, "Failed to list kubernetes deployments")
return false, err
}
@@ -202,13 +189,19 @@ func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deploymen
return r.kubeClient.Update(context.Background(), deployment)
}
func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error {
reqLog := deploymentLog.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deployment, annotations map[string]string, request reconcile.Request) error {
reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
namespace := deployment.Namespace
// check if deployment is marked to be injected with secrets via the webhook
injectedContainers, injected := annotations[op.ContainerInjectAnnotation]
if injected {
parsedInjectedContainers := strings.Split(injectedContainers, ",")
return onepassword.CreateOnePasswordItemResourceFromDeployment(r.opConnectClient, r.kubeClient, deployment, parsedInjectedContainers)
}
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
@@ -219,17 +212,5 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deploy
return fmt.Errorf("Failed to retrieve item: %v", err)
}
// Create owner reference.
gvk, err := apiutil.GVKForObject(deployment, r.scheme)
if err != nil {
return fmt.Errorf("could not to retrieve group version kind: %v", err)
}
ownerRef := &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: deployment.GetName(),
UID: deployment.GetUID(),
}
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, annotations, ownerRef)
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, annotations)
}

View File

@@ -0,0 +1,480 @@
package deployment
import (
"context"
"fmt"
"regexp"
"testing"
"github.com/1Password/onepassword-operator/operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/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,
op.NameAnnotation: name,
},
},
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,
op.NameAnnotation: name,
},
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",
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Create Deployment",
deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
}
func TestReconcileDepoyment(t *testing.T) {
for _, testData := range tests {
t.Run(testData.testName, func(t *testing.T) {
// Register operator types with the runtime scheme.
s := scheme.Scheme
s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.deploymentResource)
// Objects to track in the fake client.
objs := []runtime.Object{
testData.deploymentResource,
}
if testData.existingSecret != nil {
objs = append(objs, testData.existingSecret)
}
if testData.existingDeployment != nil {
objs = append(objs, testData.existingDeployment)
}
// Create a fake client to mock API calls.
cl := fake.NewFakeClientWithScheme(s, objs...)
// Create a Deployment object with the scheme and mock kubernetes
// and 1Password Connect client.
opConnectClient := &mocks.TestClient{}
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
item.Version = version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
r := &ReconcileDeployment{
kubeClient: cl,
scheme: s,
opConnectClient: opConnectClient,
opAnnotationRegExp: regex,
}
// Mock request to simulate Reconcile() being called on an event for a
// watched resource .
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: name,
Namespace: namespace,
},
}
_, err := r.Reconcile(req)
assert.Equal(t, testData.expectedError, err)
var expectedSecretName string
if testData.expectedResultSecret == nil {
expectedSecretName = testData.deploymentResource.Name
} else {
expectedSecretName = testData.expectedResultSecret.Name
}
// Check if Secret has been created and has the correct data
secret := &corev1.Secret{}
err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret)
if testData.expectedResultSecret == nil {
assert.Error(t, err)
assert.True(t, errors2.IsNotFound(err))
} else {
assert.Equal(t, testData.expectedResultSecret.Data, secret.Data)
assert.Equal(t, testData.expectedResultSecret.Name, secret.Name)
assert.Equal(t, testData.expectedResultSecret.Type, secret.Type)
assert.Equal(t, testData.expectedResultSecret.Annotations[op.VersionAnnotation], secret.Annotations[op.VersionAnnotation])
updatedCR := &appsv1.Deployment{}
err = cl.Get(context.TODO(), req.NamespacedName, updatedCR)
assert.NoError(t, err)
}
})
}
}
func generateFields(username, password string) []*onepassword.ItemField {
fields := []*onepassword.ItemField{
{
Label: "username",
Value: username,
},
{
Label: "password",
Value: password,
},
}
return fields
}

View File

@@ -0,0 +1,164 @@
package onepassworditem
import (
"context"
"fmt"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
kubeSecrets "github.com/1Password/onepassword-operator/operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/operator/pkg/onepassword"
op "github.com/1Password/onepassword-operator/operator/pkg/onepassword"
"github.com/1Password/onepassword-operator/operator/pkg/utils"
"github.com/1Password/connect-sdk-go/connect"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
var log = logf.Log.WithName("controller_onepassworditem")
var finalizer = "onepassword.com/finalizer.secret"
func Add(mgr manager.Manager, opConnectClient connect.Client) error {
return add(mgr, newReconciler(mgr, opConnectClient))
}
func newReconciler(mgr manager.Manager, opConnectClient connect.Client) *ReconcileOnePasswordItem {
return &ReconcileOnePasswordItem{
kubeClient: mgr.GetClient(),
scheme: mgr.GetScheme(),
opConnectClient: opConnectClient,
}
}
func add(mgr manager.Manager, r reconcile.Reconciler) error {
c, err := controller.New("onepassworditem-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to primary resource OnePasswordItem
err = c.Watch(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
return nil
}
var _ reconcile.Reconciler = &ReconcileOnePasswordItem{}
type ReconcileOnePasswordItem struct {
kubeClient kubeClient.Client
scheme *runtime.Scheme
opConnectClient connect.Client
}
func (r *ReconcileOnePasswordItem) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&onepasswordv1.OnePasswordItem{}).
Complete(r)
}
func (r *ReconcileOnePasswordItem) Reconcile(request reconcile.Request) (reconcile.Result, error) {
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling OnePasswordItem")
onepassworditem := &onepasswordv1.OnePasswordItem{}
err := r.kubeClient.Get(context.Background(), request.NamespacedName, onepassworditem)
if err != nil {
if errors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
// If the deployment is not being deleted
if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() {
// Adds a finalizer to the deployment if one does not exist.
// This is so we can handle cleanup of associated secrets properly
if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer)
if err := r.kubeClient.Update(context.Background(), onepassworditem); err != nil {
return reconcile.Result{}, err
}
}
// Handles creation or updating secrets for deployment if needed
if err := r.HandleOnePasswordItem(onepassworditem, request); err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
// If one password finalizer exists then we must cleanup associated secrets
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
// Delete associated kubernetes secret
if err = r.cleanupKubernetesSecret(onepassworditem); err != nil {
return reconcile.Result{}, err
}
// Remove finalizer now that cleanup is complete
if err := r.removeFinalizer(onepassworditem); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
func (r *ReconcileOnePasswordItem) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error {
onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer)
if err := r.kubeClient.Update(context.Background(), onePasswordItem); err != nil {
return err
}
return nil
}
func (r *ReconcileOnePasswordItem) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error {
kubernetesSecret := &corev1.Secret{}
kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name
kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace
r.kubeClient.Delete(context.Background(), kubernetesSecret)
if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil {
if !errors.IsNotFound(err) {
return err
}
}
return nil
}
func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem(opSecret *onepasswordv1.OnePasswordItem) error {
opSecret.ObjectMeta.Finalizers = utils.RemoveString(opSecret.ObjectMeta.Finalizers, finalizer)
return r.kubeClient.Update(context.Background(), opSecret)
}
func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
secretName := resource.GetName()
labels := resource.Labels
annotations := resource.Annotations
autoRestart := annotations[op.RestartDeploymentsAnnotation]
// do not create kubernetes secret if the OnePasswordItem was generated
// due to secret being injected container via webhook
_, injectedSecret := annotations[op.InjectedAnnotation]
if injectedSecret {
return nil
}
item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath)
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, annotations)
}

View File

@@ -0,0 +1,439 @@
package onepassworditem
import (
"context"
"fmt"
"testing"
"github.com/1Password/onepassword-operator/operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/operator/pkg/onepassword"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
errors2 "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubectl/pkg/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const (
onePasswordItemKind = "OnePasswordItem"
onePasswordItemAPIVersion = "onepassword.com/v1"
name = "test"
namespace = "default"
vaultId = "hfnjvi6aymbsnfc2xeeoheizda"
itemId = "nwrhuano7bcwddcviubpp4mhfq"
username = "test-user"
password = "QmHumKc$mUeEem7caHtbaBaJ"
firstHost = "http://localhost:8080"
awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
iceCream = "freezing blue 20%"
userKey = "username"
passKey = "password"
version = 123
)
type testReconcileItem struct {
testName string
customResource *onepasswordv1.OnePasswordItem
existingSecret *corev1.Secret
expectedError error
expectedResultSecret *corev1.Secret
expectedEvents []string
opItem map[string]string
existingOnePasswordItem *onepasswordv1.OnePasswordItem
}
var (
expectedSecretData = map[string][]byte{
"password": []byte(password),
"username": []byte(username),
}
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
)
var (
time = metav1.Now()
)
var tests = []testReconcileItem{
{
testName: "Test Delete OnePasswordItem",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
DeletionTimestamp: &time,
Finalizers: []string{
finalizer,
},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: nil,
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Test Do not update if OnePassword Version has not changed",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
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: "Custom secret type",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Secret from 1Password item with invalid K8s labels",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!my sECReT it3m%",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret-it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Secret from 1Password item with fields and sections that have invalid K8s labels",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!my sECReT it3m%",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret-it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: map[string][]byte{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"ice-cream-type": []byte(iceCream),
},
},
opItem: map[string]string{
userKey: username,
passKey: password,
"first host": firstHost,
"AWS Access Key": awsKey,
"😄 ice-cream type": iceCream,
},
},
{
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

@@ -5,15 +5,11 @@ import (
"fmt"
"regexp"
"strings"
"reflect"
errs "errors"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/onepassword-operator/operator/pkg/utils"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -31,11 +27,9 @@ const restartAnnotation = OnepasswordPrefix + "/last-restarted"
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable")
var log = logf.Log
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string, ownerRef *metav1.OwnerReference) error {
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretAnnotations map[string]string) error {
itemVersion := fmt.Sprint(item.Version)
@@ -55,9 +49,7 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
}
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
}
// "Opaque" and "" secret types are treated the same by Kubernetes.
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item, ownerRef)
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, *item)
currentSecret := &corev1.Secret{}
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
@@ -68,14 +60,7 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return err
}
currentAnnotations := currentSecret.Annotations
currentLabels := currentSecret.Labels
currentSecretType := string(currentSecret.Type)
if !reflect.DeepEqual(currentSecretType, secretType) {
return ErrCannotUpdateSecretType
}
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) {
if !reflect.DeepEqual(currentSecret.Annotations, secretAnnotations) || !reflect.DeepEqual(currentSecret.Labels, labels) {
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
currentSecret.ObjectMeta.Annotations = secretAnnotations
currentSecret.ObjectMeta.Labels = labels
@@ -87,26 +72,19 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return nil
}
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret {
var ownerRefs []metav1.OwnerReference
if ownerRef != nil {
ownerRefs = []metav1.OwnerReference{*ownerRef}
}
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, item onepassword.Item) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: formatSecretName(name),
Namespace: namespace,
Annotations: annotations,
Labels: labels,
OwnerReferences: ownerRefs,
Name: utils.FormatSecretName(name),
Namespace: namespace,
Annotations: annotations,
Labels: labels,
},
Data: BuildKubernetesSecretData(item.Fields, item.Files),
Type: corev1.SecretType(secretType),
Data: BuildKubernetesSecretData(item.Fields),
}
}
func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte {
func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byte {
secretData := map[string][]byte{}
for i := 0; i < len(fields); i++ {
if fields[i].Value != "" {
@@ -114,37 +92,9 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepass
secretData[key] = []byte(fields[i].Value)
}
}
// populate unpopulated fields from files
for _, file := range files {
content, err := file.Content()
if err != nil {
log.Error(err, "Could not load contents of file %s", file.Name)
continue
}
if content != nil {
key := file.Name
if secretData[key] == nil {
secretData[key] = content
} else {
log.Info(fmt.Sprintf("File '%s' ignored because of a field with the same name", file.Name))
}
}
}
return secretData
}
// formatSecretName rewrites a value to be a valid Secret name.
//
// The Secret meta.name and data keys must be valid DNS subdomain names
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
func formatSecretName(value string) string {
if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 {
return value
}
return createValidSecretName(value)
}
// formatSecretDataName rewrites a value to be a valid Secret data key.
//
// The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.`
@@ -156,20 +106,6 @@ func formatSecretDataName(value string) string {
return createValidSecretDataName(value)
}
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+")
func createValidSecretName(value string) string {
result := strings.ToLower(value)
result = invalidDNS1123Chars.ReplaceAllString(result, "-")
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
}
// first and last character MUST be alphanumeric
return strings.Trim(result, "-.")
}
var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+")
var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)")

View File

@@ -8,7 +8,6 @@ import (
"github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes"
@@ -36,9 +35,7 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
secretType := ""
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil)
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -56,54 +53,6 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
}
}
func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
secretName := "test-secret-name"
namespace := "test"
item := onepassword.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient()
secretLabels := map[string]string{}
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
secretType := ""
ownerRef := &metav1.OwnerReference{
Kind: "Deployment",
APIVersion: "apps/v1",
Name: "test-deployment",
UID: types.UID("test-uid"),
}
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, ownerRef)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
createdSecret := &corev1.Secret{}
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
// Check owner references.
gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences
if len(gotOwnerRefs) != 1 {
t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs))
}
expOwnerRef := metav1.OwnerReference{
Kind: "Deployment",
APIVersion: "apps/v1",
Name: "test-deployment",
UID: types.UID("test-uid"),
}
gotOwnerRef := gotOwnerRefs[0]
if gotOwnerRef != expOwnerRef {
t.Errorf("Expected owner reference value: %v but got: %v", expOwnerRef, gotOwnerRef)
}
}
func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
secretName := "test-secret-update"
namespace := "test"
@@ -117,10 +66,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
kubeClient := fake.NewFakeClient()
secretLabels := map[string]string{}
secretAnnotations := map[string]string{}
secretType := ""
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil)
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -131,7 +77,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
newItem.Version = 456
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil)
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretAnnotations)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -147,7 +93,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
func TestBuildKubernetesSecretData(t *testing.T) {
fields := generateFields(5)
secretData := BuildKubernetesSecretData(fields, nil)
secretData := BuildKubernetesSecretData(fields)
if len(secretData) != len(fields) {
t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData))
}
@@ -165,9 +111,8 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
item := onepassword.Item{}
item.Fields = generateFields(5)
labels := map[string]string{}
secretType := ""
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item)
if kubeSecret.Name != strings.ToLower(name) {
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
}
@@ -189,7 +134,6 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
}
labels := map[string]string{}
item := onepassword.Item{}
secretType := ""
item.Fields = []*onepassword.ItemField{
{
@@ -202,7 +146,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
},
}
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item)
// Assert Secret's meta.name was fixed
if kubeSecret.Name != expectedName {
@@ -220,39 +164,6 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
}
}
func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
secretName := "tls-test-secret-name"
namespace := "test"
item := onepassword.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient()
secretLabels := map[string]string{}
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
secretType := "kubernetes.io/tls"
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
createdSecret := &corev1.Secret{}
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
if err != nil {
t.Errorf("Secret was not created: %v", err)
}
if createdSecret.Type != corev1.SecretTypeTLS {
t.Errorf("Expected secretType to be of tyype corev1.SecretTypeTLS, got %s", string(createdSecret.Type))
}
}
func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) {
actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
if err != nil {

View File

@@ -7,7 +7,6 @@ import (
type TestClient struct {
GetVaultsFunc func() ([]onepassword.Vault, error)
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetVaultFunc func(uuid string) (*onepassword.Vault, error)
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
@@ -15,14 +14,11 @@ type TestClient struct {
CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
GetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error)
GetFileContentFunc func(file *onepassword.File) ([]byte, error)
}
var (
GetGetVaultsFunc func() ([]onepassword.Vault, error)
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
DoGetVaultFunc func(uuid string) (*onepassword.Vault, error)
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
@@ -30,8 +26,6 @@ var (
DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
DoUpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
DoGetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error)
DoGetFileContentFunc func(file *onepassword.File) ([]byte, error)
)
// Do is the mock client's `Do` func
@@ -43,10 +37,6 @@ func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error)
return DoGetVaultsByTitleFunc(title)
}
func (m *TestClient) GetVault(uuid string) (*onepassword.Vault, error) {
return DoGetVaultFunc(uuid)
}
func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
return GetGetItemFunc(uuid, vaultUUID)
}
@@ -74,11 +64,3 @@ func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
return DoUpdateItemFunc(item, vaultUUID)
}
func (m *TestClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) {
return DoGetFileFunc(uuid, itemUUID, vaultUUID)
}
func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) {
return DoGetFileContentFunc(file)
}

View File

@@ -14,6 +14,8 @@ const (
VersionAnnotation = OnepasswordPrefix + "/item-version"
RestartAnnotation = OnepasswordPrefix + "/last-restarted"
RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
ContainerInjectAnnotation = OnepasswordPrefix + "/inject"
InjectedAnnotation = OnepasswordPrefix + "/injected"
)
func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Regexp) (map[string]string, bool) {

View File

@@ -1,6 +1,8 @@
package onepassword
import (
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
"github.com/1Password/onepassword-operator/operator/pkg/utils"
corev1 "k8s.io/api/core/v1"
)
@@ -15,15 +17,6 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string
}
}
}
envFromVariables := containers[i].EnvFrom
for j := 0; j < len(envFromVariables); j++ {
if envFromVariables[j].SecretRef != nil {
_, ok := secrets[envFromVariables[j].SecretRef.Name]
if ok {
return true
}
}
}
}
return false
}
@@ -39,15 +32,32 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st
}
}
}
envFromVariables := containers[i].EnvFrom
for j := 0; j < len(envFromVariables); j++ {
if envFromVariables[j].SecretRef != nil {
secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name]
if ok {
updatedDeploymentSecrets[secret.Name] = secret
}
}
}
}
return updatedDeploymentSecrets
}
func AreContainersUsingInjectedSecrets(containers []corev1.Container, injectedContainers []string, items map[string]*onepasswordv1.OnePasswordItem) bool {
for _, container := range containers {
envVariables := container.Env
// check if container was set to be injected with secrets
for _, injectedContainer := range injectedContainers {
if injectedContainer != container.Name {
continue
}
}
// check if any environment variables are using an updated injected secret
for _, envVariable := range envVariables {
referenceVault, referenceItem, err := ParseReference(envVariable.Value)
if err != nil {
continue
}
_, itemFound := items[utils.BuildInjectedOnePasswordItemName(referenceVault, referenceItem)]
if itemFound {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,43 @@
package onepassword
import (
"testing"
corev1 "k8s.io/api/core/v1"
)
func TestAreContainersUsingSecrets(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{},
"onepassword-api-key": &corev1.Secret{},
}
containerSecretNames := []string{
"onepassword-database-secret",
"onepassword-api-key",
"some_other_key",
}
containers := generateContainers(containerSecretNames)
if !AreContainersUsingSecrets(containers, secretNamesToSearch) {
t.Errorf("Expected that containers were using secrets but they were not detected.")
}
}
func TestAreContainersNotUsingSecrets(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{},
"onepassword-api-key": &corev1.Secret{},
}
containerSecretNames := []string{
"some_other_key",
}
containers := generateContainers(containerSecretNames)
if AreContainersUsingSecrets(containers, secretNamesToSearch) {
t.Errorf("Expected that containers were not using secrets but they were detected.")
}
}

View File

@@ -1,6 +1,9 @@
package onepassword
import (
"strings"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
)
@@ -24,3 +27,14 @@ func GetUpdatedSecretsForDeployment(deployment *appsv1.Deployment, secrets map[s
return updatedSecretsForDeployment
}
func IsDeploymentUsingInjectedSecrets(deployment *appsv1.Deployment, items map[string]*onepasswordv1.OnePasswordItem) bool {
containers := deployment.Spec.Template.Spec.Containers
containers = append(containers, deployment.Spec.Template.Spec.InitContainers...)
injectedContainers, enabled := deployment.Spec.Template.Annotations[ContainerInjectAnnotation]
if !enabled {
return false
}
parsedInjectedContainers := strings.Split(injectedContainers, ",")
return AreContainersUsingInjectedSecrets(containers, parsedInjectedContainers, items)
}

View File

@@ -39,7 +39,7 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) {
}
deployment := &appsv1.Deployment{}
deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames)
deployment.Spec.Template.Spec.Containers = generateContainers(containerSecretNames)
if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) {
t.Errorf("Expected that deployment was using secrets but they were not detected.")
}

View File

@@ -11,6 +11,16 @@ import (
var logger = logf.Log.WithName("retrieve_item")
const secretReferencePrefix = "op://"
type InvalidOPFormatError struct {
Reference string
}
func (e *InvalidOPFormatError) Error() string {
return fmt.Sprintf("Invalid secret reference : %s. Secret references should start with op://", e.Reference)
}
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
if err != nil {
@@ -30,15 +40,31 @@ func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*one
if err != nil {
return nil, err
}
return item, nil
}
for _, file := range item.Files {
_, err := opConnectClient.GetFileContent(file)
if err != nil {
return nil, err
}
func ParseReference(reference string) (string, string, error) {
if !strings.HasPrefix(reference, secretReferencePrefix) {
return "", "", &InvalidOPFormatError{Reference: reference}
}
path := strings.TrimPrefix(reference, secretReferencePrefix)
splitPath := strings.Split(path, "/")
if len(splitPath) != 3 {
return "", "", fmt.Errorf("Invalid secret reference : %s. Secret references should match op://<vault>/<item>/<field>", reference)
}
return item, nil
vault := splitPath[0]
if vault == "" {
return "", "", fmt.Errorf("Invalid secret reference : %s. Vault can't be empty.", reference)
}
item := splitPath[1]
if item == "" {
return "", "", fmt.Errorf("Invalid secret reference : %s. Item can't be empty.", reference)
}
return vault, item, nil
}
func ParseVaultAndItemFromPath(path string) (string, string, error) {

View File

@@ -17,7 +17,8 @@ func generateVolumes(names []string) []corev1.Volume {
}
return volumes
}
func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container {
func generateContainers(names []string) []corev1.Container {
containers := []corev1.Container{}
for i := 0; i < len(names); i++ {
container := corev1.Container{
@@ -39,16 +40,3 @@ func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container
}
return containers
}
func generateContainersWithSecretRefsFromEnvFrom(names []string) []corev1.Container {
containers := []corev1.Container{}
for i := 0; i < len(names); i++ {
container := corev1.Container{
EnvFrom: []corev1.EnvFromSource{
{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: names[i]}}},
},
}
containers = append(containers, container)
}
return containers
}

View File

@@ -0,0 +1,95 @@
package onepassword
import (
"context"
"errors"
"fmt"
"github.com/1Password/connect-sdk-go/connect"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
"github.com/1Password/onepassword-operator/operator/pkg/utils"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client"
)
func CreateOnePasswordItemResourceFromDeployment(opClient connect.Client, kubeClient kubernetesClient.Client, deployment *appsv1.Deployment, injectedContainers []string) error {
containers := deployment.Spec.Template.Spec.Containers
containers = append(containers, deployment.Spec.Template.Spec.InitContainers...)
for _, container := range containers {
// check if container is listed is one of the containers
// set to have injected secrets
for _, injectedContainer := range injectedContainers {
if injectedContainer != container.Name {
continue
}
// create a one password item custom resource to track updates for injected secrets
err := CreateOnePasswordCRSecretsFromContainer(opClient, kubeClient, container, deployment.Namespace)
if err != nil {
return err
}
}
}
return nil
}
func CreateOnePasswordCRSecretsFromContainer(opClient connect.Client, kubeClient kubernetesClient.Client, container corev1.Container, namespace string) error {
for _, env := range container.Env {
// if value is not of format op://<vault>/<item>/<field> then ignore
vault, item, err := ParseReference(env.Value)
if err != nil {
var ev *InvalidOPFormatError
if !errors.As(err, &ev) {
return err
}
continue
}
// create a one password item custom resource to track updates for injected secrets
err = CreateOnePasswordCRSecretFromReference(opClient, kubeClient, vault, item, namespace)
if err != nil {
return err
}
}
return nil
}
func CreateOnePasswordCRSecretFromReference(opClient connect.Client, kubeClient kubernetesClient.Client, vault, item, namespace string) error {
retrievedItem, err := GetOnePasswordItemByPath(opClient, fmt.Sprintf("vaults/%s/items/%s", vault, item))
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}
name := utils.BuildInjectedOnePasswordItemName(vault, item)
onepassworditem := BuildOnePasswordItemCRFromPath(vault, item, name, namespace, fmt.Sprint(retrievedItem.Version))
currentOnepassworditem := &onepasswordv1.OnePasswordItem{}
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: onepassworditem.Name, Namespace: onepassworditem.Namespace}, currentOnepassworditem)
if k8sErrors.IsNotFound(err) {
log.Info(fmt.Sprintf("Creating OnePasswordItem CR %v at namespace '%v'", onepassworditem.Name, onepassworditem.Namespace))
return kubeClient.Create(context.Background(), onepassworditem)
} else if err != nil {
return err
}
return nil
}
func BuildOnePasswordItemCRFromPath(vault, item, name, namespace, version string) *onepasswordv1.OnePasswordItem {
return &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
InjectedAnnotation: "true",
VersionAnnotation: version,
},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: fmt.Sprintf("vaults/%s/items/%s", vault, item),
},
}
}

View File

@@ -0,0 +1,234 @@
package onepassword
import (
"context"
"fmt"
"strings"
"testing"
"github.com/1Password/onepassword-operator/operator/pkg/mocks"
"github.com/1Password/connect-sdk-go/onepassword"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
"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"
)
type onepassworditemInjections struct {
testName string
existingDeployment *appsv1.Deployment
existingNamespace *corev1.Namespace
expectedError error
expectedEvents []string
opItem map[string]string
expectedOPItem *onepasswordv1.OnePasswordItem
}
var onepassworditemTests = []onepassworditemInjections{
{
testName: "Try to Create OnePasswordItem with container with valid op reference",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
ContainerInjectAnnotation: "test-app",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test-app",
Env: []corev1.EnvVar{
{
Name: name,
Value: fmt.Sprintf("op://%s/%s/test", vaultId, itemId),
},
},
},
},
},
},
},
},
expectedError: nil,
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedOPItem: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: "OnePasswordItem",
APIVersion: "onepassword.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: injectedOnePasswordItemName,
Namespace: namespace,
Annotations: map[string]string{
InjectedAnnotation: "true",
VersionAnnotation: "old",
},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
},
{
testName: "Container with no op:// reference does not create OnePasswordItem",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
ContainerInjectAnnotation: "test-app",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test-app",
Env: []corev1.EnvVar{
{
Name: name,
Value: fmt.Sprintf("some value"),
},
},
},
},
},
},
},
},
expectedError: nil,
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedOPItem: nil,
},
{
testName: "Container with op:// reference missing vault and item does not create OnePasswordItem and returns error",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
ContainerInjectAnnotation: "test-app",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test-app",
Env: []corev1.EnvVar{
{
Name: name,
Value: fmt.Sprintf("op://"),
},
},
},
},
},
},
},
},
expectedError: fmt.Errorf("Invalid secret reference : %s. Secret references should match op://<vault>/<item>/<field>", "op://"),
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedOPItem: nil,
},
}
func TestOnePasswordItemSecretInjected(t *testing.T) {
for _, testData := range onepassworditemTests {
t.Run(testData.testName, func(t *testing.T) {
// Register operator types with the runtime scheme.
s := scheme.Scheme
s.AddKnownTypes(appsv1.SchemeGroupVersion, &onepasswordv1.OnePasswordItem{}, &onepasswordv1.OnePasswordItemList{}, &appsv1.Deployment{})
// Objects to track in the fake client.
objs := []runtime.Object{
testData.existingDeployment,
testData.existingNamespace,
}
// Create a fake client to mock API calls.
cl := fake.NewFakeClientWithScheme(s, objs...)
opConnectClient := &mocks.TestClient{}
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
item.Version = itemVersion
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
injectedContainers := testData.existingDeployment.Spec.Template.ObjectMeta.Annotations[ContainerInjectAnnotation]
parsedInjectedContainers := strings.Split(injectedContainers, ",")
err := CreateOnePasswordItemResourceFromDeployment(opConnectClient, cl, testData.existingDeployment, parsedInjectedContainers)
assert.Equal(t, testData.expectedError, err)
// Check if Secret has been created and has the correct data
opItemCR := &onepasswordv1.OnePasswordItem{}
err = cl.Get(context.TODO(), types.NamespacedName{Name: injectedOnePasswordItemName, Namespace: namespace}, opItemCR)
if testData.expectedOPItem == nil {
assert.Error(t, err)
assert.True(t, errors2.IsNotFound(err))
} else {
assert.Equal(t, testData.expectedOPItem.Spec.ItemPath, opItemCR.Spec.ItemPath)
assert.Equal(t, testData.expectedOPItem.Name, opItemCR.Name)
assert.Equal(t, testData.expectedOPItem.Annotations[InjectedAnnotation], opItemCR.Annotations[InjectedAnnotation])
}
})
}
}

View File

@@ -5,10 +5,9 @@ import (
"fmt"
"time"
v1 "github.com/1Password/onepassword-operator/api/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/utils"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
kubeSecrets "github.com/1Password/onepassword-operator/operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/operator/pkg/utils"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
@@ -43,12 +42,17 @@ func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error {
return err
}
return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets)
updatedInjectedSecrets, err := h.updateInjectedSecrets()
if err != nil {
return err
}
return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets, updatedInjectedSecrets)
}
func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret) error {
// No secrets to update. Exit
if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil {
func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret, updatedInjectedSecretsByNamespace map[string]map[string]*onepasswordv1.OnePasswordItem) error {
if len(updatedSecretsByNamespace) == 0 && len(updatedInjectedSecretsByNamespace) == 0 {
return nil
}
@@ -65,6 +69,7 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap()
if err != nil {
log.Error(err, "Error determining which namespaces allow restarts")
return err
}
@@ -72,17 +77,24 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
deployment := &deployments.Items[i]
updatedSecrets := updatedSecretsByNamespace[deployment.Namespace]
// check if deployment is using one of the updated secrets
updatedDeploymentSecrets := GetUpdatedSecretsForDeployment(deployment, updatedSecrets)
if len(updatedDeploymentSecrets) == 0 {
continue
}
for _, secret := range updatedDeploymentSecrets {
if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) {
h.restartDeployment(deployment)
continue
if len(updatedDeploymentSecrets) != 0 {
for _, secret := range updatedDeploymentSecrets {
if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) {
h.restartDeployment(deployment)
continue
}
}
}
// check if the deployment is using one of the updated injected secrets
updatedInjection := IsDeploymentUsingInjectedSecrets(deployment, updatedInjectedSecretsByNamespace[deployment.Namespace])
if updatedInjection && isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespaceMap) {
h.restartDeployment(deployment)
continue
}
log.Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace))
}
@@ -91,9 +103,10 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) {
log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace))
deployment.Spec.Template.Annotations = map[string]string{
RestartAnnotation: time.Now().String(),
if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = map[string]string{}
}
deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String()
err := h.client.Update(context.Background(), deployment)
if err != nil {
log.Error(err, "Problem restarting deployment")
@@ -118,30 +131,22 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
continue
}
OnePasswordItemPath := h.getPathFromOnePasswordItem(secret)
item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath)
item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation])
if err != nil {
log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name)
continue
return nil, fmt.Errorf("Failed to retrieve item: %v", err)
}
itemVersion := fmt.Sprint(item.Version)
itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString {
if currentVersion != itemVersion {
if isItemLockedForForcedRestarts(item) {
log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName()))
secret.Annotations[VersionAnnotation] = itemVersion
secret.Annotations[ItemPathAnnotation] = itemPathString
h.client.Update(context.Background(), &secret)
continue
}
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
secret.Annotations[VersionAnnotation] = itemVersion
secret.Annotations[ItemPathAnnotation] = itemPathString
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, string(secret.Type), *item, nil)
log.Info(fmt.Sprintf("New secret path: %v and version: %v", updatedSecret.Annotations[ItemPathAnnotation], updatedSecret.Annotations[VersionAnnotation]))
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, *item)
h.client.Update(context.Background(), updatedSecret)
if updatedSecrets[secret.Namespace] == nil {
updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)
@@ -152,6 +157,52 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
return updatedSecrets, nil
}
func (h *SecretUpdateHandler) updateInjectedSecrets() (map[string]map[string]*onepasswordv1.OnePasswordItem, error) {
// fetch all onepassworditems
onepasswordItems := &onepasswordv1.OnePasswordItemList{}
err := h.client.List(context.Background(), onepasswordItems)
if err != nil {
log.Error(err, "Failed to list OnePasswordItems")
return nil, err
}
updatedItems := map[string]map[string]*onepasswordv1.OnePasswordItem{}
for _, item := range onepasswordItems.Items {
// if onepassworditem was not generated by injecting a secret into a deployment then ignore
_, injected := item.Annotations[InjectedAnnotation]
if !injected {
continue
}
itemPath := item.Spec.ItemPath
currentVersion := item.Annotations[VersionAnnotation]
if len(itemPath) == 0 || len(currentVersion) == 0 {
continue
}
storedItem, err := GetOnePasswordItemByPath(h.opConnectClient, itemPath)
if err != nil {
return nil, fmt.Errorf("Failed to retrieve item: %v", err)
}
itemVersion := fmt.Sprint(storedItem.Version)
if currentVersion != itemVersion {
item.Annotations[VersionAnnotation] = itemVersion
h.client.Update(context.Background(), &item)
if isItemLockedForForcedRestarts(storedItem) {
log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a OnePasswordItem secret or a rolling restart.", item.Name))
continue
}
if updatedItems[item.Namespace] == nil {
updatedItems[item.Namespace] = make(map[string]*onepasswordv1.OnePasswordItem)
}
updatedItems[item.Namespace][item.Name] = &item
}
}
return updatedItems, nil
}
func isItemLockedForForcedRestarts(item *onepassword.Item) bool {
tags := item.Tags
for i := 0; i < len(tags); i++ {
@@ -186,25 +237,9 @@ func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string
return namespacesMap, nil
}
func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string {
onePasswordItem := &v1.OnePasswordItem{}
// Search for our original OnePasswordItem if it exists
err := h.client.Get(context.TODO(), client.ObjectKey{
Namespace: secret.Namespace,
Name: secret.Name}, onePasswordItem)
if err == nil {
return onePasswordItem.Spec.ItemPath
}
// If we can't find the OnePassword Item we'll just return the annotation from the secret item.
return secret.Annotations[ItemPathAnnotation]
}
func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
restartDeployment := secret.Annotations[RestartDeploymentsAnnotation]
//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
//If annotation for auto restarts for deployment is not set. Check for the annotation on its deployment
if restartDeployment == "" {
return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace)
}

View File

@@ -5,9 +5,10 @@ import (
"fmt"
"testing"
"github.com/1Password/onepassword-operator/pkg/mocks"
"github.com/1Password/onepassword-operator/operator/pkg/mocks"
"github.com/1Password/connect-sdk-go/onepassword"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
@@ -20,23 +21,25 @@ import (
)
const (
deploymentKind = "Deployment"
deploymentAPIVersion = "v1"
name = "test-deployment"
namespace = "default"
vaultId = "hfnjvi6aymbsnfc2xeeoheizda"
itemId = "nwrhuano7bcwddcviubpp4mhfq"
username = "test-user"
password = "QmHumKc$mUeEem7caHtbaBaJ"
userKey = "username"
passKey = "password"
itemVersion = 123
deploymentKind = "Deployment"
deploymentAPIVersion = "v1"
name = "test-deployment"
namespace = "default"
vaultId = "hfnjvi6aymbsnfc2xeeoheizda"
itemId = "nwrhuano7bcwddcviubpp4mhfq"
username = "test-user"
password = "QmHumKc$mUeEem7caHtbaBaJ"
userKey = "username"
passKey = "password"
itemVersion = 123
injectedOnePasswordItemName = "injectedsecret-" + vaultId + "-" + itemId
)
type testUpdateSecretTask struct {
testName string
existingDeployment *appsv1.Deployment
existingNamespace *corev1.Namespace
existingOnePasswordItem *onepasswordv1.OnePasswordItem
existingSecret *corev1.Secret
expectedError error
expectedResultSecret *corev1.Secret
@@ -755,6 +758,123 @@ var tests = []testUpdateSecretTask{
expectedRestart: true,
globalAutoRestartEnabled: false,
},
{
testName: "OP item has new version. Secret needs update. Deployment is restarted based on injected secrets in containers",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
ContainerInjectAnnotation: "test-app",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test-app",
Env: []corev1.EnvVar{
{
Name: name,
Value: fmt.Sprintf("op://%s/%s/test", vaultId, itemId),
},
},
},
},
},
},
},
},
existingOnePasswordItem: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: "OnePasswordItem",
APIVersion: "onepassword.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: injectedOnePasswordItemName,
Namespace: namespace,
Annotations: map[string]string{
InjectedAnnotation: "true",
VersionAnnotation: "old",
},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
expectedError: nil,
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedRestart: true,
globalAutoRestartEnabled: true,
},
{
testName: "OP item has new version. Secret needs update. Deployment does not have a inject annotation",
existingNamespace: defaultNamespace,
existingDeployment: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test-app",
Env: []corev1.EnvVar{
{
Name: name,
Value: fmt.Sprintf("op://%s/%s/test", vaultId, itemId),
},
},
},
},
},
},
},
},
existingOnePasswordItem: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: "OnePasswordItem",
APIVersion: "onepassword.com/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s", vaultId, itemId),
Namespace: namespace,
Annotations: map[string]string{
InjectedAnnotation: "true",
VersionAnnotation: "old",
},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
expectedError: nil,
opItem: map[string]string{
userKey: username,
passKey: password,
},
expectedRestart: false,
globalAutoRestartEnabled: true,
},
}
func TestUpdateSecretHandler(t *testing.T) {
@@ -763,7 +883,7 @@ func TestUpdateSecretHandler(t *testing.T) {
// Register operator types with the runtime scheme.
s := scheme.Scheme
s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.existingDeployment)
s.AddKnownTypes(appsv1.SchemeGroupVersion, &onepasswordv1.OnePasswordItem{}, &onepasswordv1.OnePasswordItemList{}, &appsv1.Deployment{})
// Objects to track in the fake client.
objs := []runtime.Object{
@@ -775,6 +895,10 @@ func TestUpdateSecretHandler(t *testing.T) {
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...)
@@ -825,9 +949,9 @@ func TestUpdateSecretHandler(t *testing.T) {
_, ok := deployment.Spec.Template.Annotations[RestartAnnotation]
if ok {
assert.True(t, testData.expectedRestart, "Expected deployment to restart but it did not")
assert.True(t, testData.expectedRestart, "Deployment was restarted but should not have been.")
} else {
assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.")
assert.False(t, testData.expectedRestart, "Expected deployment to restart but it did not")
}
})
}

View File

@@ -0,0 +1,66 @@
package utils
import (
"fmt"
"regexp"
"strconv"
"strings"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
)
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+")
func ContainsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
func RemoveString(slice []string, s string) (result []string) {
for _, item := range slice {
if item == s {
continue
}
result = append(result, item)
}
return
}
func StringToBool(str string) (bool, error) {
restartDeploymentBool, err := strconv.ParseBool(strings.ToLower(str))
if err != nil {
return false, err
}
return restartDeploymentBool, nil
}
// formatSecretName rewrites a value to be a valid Secret name.
//
// The Secret meta.name and data keys must be valid DNS subdomain names
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
func FormatSecretName(value string) string {
if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 {
return value
}
return CreateValidSecretName(value)
}
func CreateValidSecretName(value string) string {
result := strings.ToLower(value)
result = invalidDNS1123Chars.ReplaceAllString(result, "-")
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
}
// first and last character MUST be alphanumeric
return strings.Trim(result, "-.")
}
func BuildInjectedOnePasswordItemName(vaultId, injectedId string) string {
return FormatSecretName(fmt.Sprintf("injectedsecret-%s-%s", vaultId, injectedId))
}

View File

@@ -0,0 +1,5 @@
package version
var (
Version = "0.0.1"
)

View File

@@ -1,85 +0,0 @@
package onepassword
import (
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestAreContainersUsingSecretsFromEnv(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{},
"onepassword-api-key": &corev1.Secret{},
}
containerSecretNames := []string{
"onepassword-database-secret",
"onepassword-api-key",
"some_other_key",
}
containers := generateContainersWithSecretRefsFromEnv(containerSecretNames)
if !AreContainersUsingSecrets(containers, secretNamesToSearch) {
t.Errorf("Expected that containers were using secrets but they were not detected.")
}
}
func TestAreContainersUsingSecretsFromEnvFrom(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": {},
"onepassword-api-key": {},
}
containerSecretNames := []string{
"onepassword-database-secret",
"onepassword-api-key",
"some_other_key",
}
containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames)
if !AreContainersUsingSecrets(containers, secretNamesToSearch) {
t.Errorf("Expected that containers were using secrets but they were not detected.")
}
}
func TestAreContainersNotUsingSecrets(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": {},
"onepassword-api-key": {},
}
containerSecretNames := []string{
"some_other_key",
}
containers := generateContainersWithSecretRefsFromEnv(containerSecretNames)
if AreContainersUsingSecrets(containers, secretNamesToSearch) {
t.Errorf("Expected that containers were not using secrets but they were detected.")
}
}
func TestAppendUpdatedContainerSecretsParsesEnvFromEnv(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": {},
"onepassword-api-key": {ObjectMeta: metav1.ObjectMeta{Name: "onepassword-api-key"}},
}
containerSecretNames := []string{
"onepassword-api-key",
}
containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames)
updatedDeploymentSecrets := map[string]*corev1.Secret{}
updatedDeploymentSecrets = AppendUpdatedContainerSecrets(containers, secretNamesToSearch, updatedDeploymentSecrets)
secretKeyName := "onepassword-api-key"
if updatedDeploymentSecrets[secretKeyName] != secretNamesToSearch[secretKeyName] {
t.Errorf("Expected that updated Secret from envfrom is found.")
}
}

View File

@@ -1,45 +0,0 @@
package utils
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
var ForceRunModeEnv = "OSDK_FORCE_RUN_MODE"
type RunModeType string
const (
LocalRunMode RunModeType = "local"
ClusterRunMode RunModeType = "cluster"
)
// 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 := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
if err != nil {
if os.IsNotExist(err) {
return "", ErrNoNamespace
}
return "", err
}
ns := strings.TrimSpace(string(nsBytes))
return ns, nil
}
func isRunModeLocal() bool {
return os.Getenv(ForceRunModeEnv) == string(LocalRunMode)
}

View File

@@ -1,33 +0,0 @@
package utils
import (
"strconv"
"strings"
)
func ContainsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
func RemoveString(slice []string, s string) (result []string) {
for _, item := range slice {
if item == s {
continue
}
result = append(result, item)
}
return
}
func StringToBool(str string) (bool, error) {
restartDeploymentBool, err := strconv.ParseBool(strings.ToLower(str))
if err != nil {
return false, err
}
return restartDeploymentBool, nil
}

View File

@@ -0,0 +1,30 @@
# Build the manager binary
FROM golang:1.17 as builder
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# Copy the go source
COPY secret-injector/cmd/main.go secret-injector/main.go
COPY secret-injector/pkg/ secret-injector/pkg/
COPY vendor/ vendor/
# Build
ARG secret_injector_version=dev
RUN CGO_ENABLED=0 \
GO111MODULE=on \
go build \
-ldflags "-X \"github.com/1Password/onepassword-operator/secret-injector/version.Version=$secret_injector_version\"" \
-mod vendor \
-a -o injector secret-injector/main.go
# Use distroless as minimal base image to package the secret-injector binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/injector .
USER nonroot:nonroot
ENTRYPOINT ["/injector"]

123
secret-injector/README.md Normal file
View File

@@ -0,0 +1,123 @@
# 1Password Secrets Injector for Kubernetes
The 1Password Secrets Injector implements a mutating webhook to inject 1Password secrets as environment variables into a pod or deployment. Unlike the 1Password Kubernetes Operator, the Secrets Injector does not create a Kubernetes Secret when assigning secrets to your resource.
## Use with the 1Password Kubernetes Operator
The 1Password Secrets Injector for Kubernetes can be used in conjuction with the 1Password Kubernetes Operator in order to provide automatic deployment restarts when a 1Password item being used by your deployment has been updated.
[Click here for more details on the 1Password Kubernetes Operator](../operator/README.md)
## Setup and Deployment
The 1Password Secrets Injector for Kubernetes uses a webhook server in order to inject secrets into pods and deployments. Admission to the webhook server must be a secure operation, thus communication with the webhook server requires a TLS certificate signed by a Kubernetes CA.
For managing TLS certifcates for your cluster please see the [official documentation](https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/). The certificate and key generated in the offical documentation must be set in the [deployment](deploy/deployment.yaml) arguments (`tlsCertFile` and `tlsKeyFile` respectively) for the Secret injector.
In additon to setting the tlsCert and tlsKey for the Secret Injector service, we must also create a webhook configuration for the service. An example of the confiugration can be found [here](deploy/mutatingwebhook.yaml). In the provided example you may notice that the caBundle is not set. Please replace this value with your caBundle. This can be generated with the Kubernetes apiserver's default caBundle with the following command
```export CA_BUNDLE=$(kubectl get configmap -n kube-system extension-apiserver-authentication -o=jsonpath='{.data.client-ca-file}' | base64 | tr -d '\n')```
```
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: op-secret-injector-webhook-config
labels:
app: op-secret-injector
webhooks:
- name: op-secret-injector.1password
failurePolicy: Fail
clientConfig:
service:
name: op-secret-injector-webhook-service
namespace: op-secret-injector
path: "/inject"
caBundle: ${CA_BUNDLE} //replace this with your own CA Bundle
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
namespaceSelector:
matchLabels:
op-secret-injection: enabled
```
You can automate this step using the script by [morvencao](https://github.com/morvencao/kube-mutating-webhook-tutorial).
```
cat deploy/mutatingwebhook.yaml | \
deploy/webhook-patch-ca-bundle.sh > \
deploy/mutatingwebhook-ca-bundle.yaml
```
Lastly, we must apply the deployment, service, and mutating webhook configuration to kubernetes:
```
kubectl create -f deploy/deployment.yaml
kubectl create -f deploy/service.yaml
kubectl create -f deploy/mutatingwebhook-ca-bundle.yaml
```
## Usage
For every namespace you want the 1Password Secret Injector to inject secrets for, you must add the label `op-secret-injection=enabled` label to the namespace:
```
kubectl label namespace <namespace> op-secret-injection=enabled
```
To inject a 1Password secret as an environment variable, your pod or deployment you must add an environment variable to the resource with a value referencing your 1Password item in the format `op://<vault>/<item>[/section]/<field>`. You must also annotate your pod/deployment spec with `operator.1password.io/inject` which expects a comma separated list of the names of the containers to that will be mutated and have secrets injected.
Note: You must also include the command needed to run the container as the secret injector prepends a script to this command in order to allow for secret injection.
```
#example
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-example
spec:
selector:
matchLabels:
app: app-example
template:
metadata:
annotations:
operator.1password.io/inject: "app-example,another-example"
labels:
app: app-example
spec:
containers:
- name: app-example
image: my-image
command: ["./example"]
env:
- name: DB_USERNAME
value: op://my-vault/my-item/sql/username
- name: DB_PASSWORD
value: op://my-vault/my-item/sql/password
- name: another-example
image: my-image
env:
- name: DB_USERNAME
value: op://my-vault/my-item/sql/username
- name: DB_PASSWORD
value: op://my-vault/my-item/sql/password
- name: my-app //because my-app is not listed in the inject annotation above this container will not be injected with secrets
image: my-image
env:
- name: DB_USERNAME
value: op://my-vault/my-item/sql/username
- name: DB_PASSWORD
value: op://my-vault/my-item/sql/password
```
## Troubleshooting
If you are trouble getting secrets injected in your pod, check the following:
1. Check that that the namespace of your pod has the `op-secret-injection=enabled` label
2. Check that the `caBundle` in `mutatingwebhookconfiguration.yaml` is set with a correct value
3. Ensure that the 1Password Secret Injector webhook is running (`op-secret-injector` by default).
4. Check that your container has a `command` field specifying the command to run the app in your container

View File

@@ -0,0 +1,87 @@
package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/1Password/onepassword-operator/secret-injector/pkg/webhook"
"github.com/golang/glog"
)
const (
connectTokenSecretKeyEnv = "OP_CONNECT_TOKEN_KEY"
connectTokenSecretNameEnv = "OP_CONNECT_TOKEN_NAME"
connectHostEnv = "OP_CONNECT_HOST"
)
func main() {
var parameters webhook.SecretInjectorParameters
glog.Info("Starting webhook")
// get command line parameters
flag.IntVar(&parameters.Port, "port", 8443, "Webhook server port.")
flag.StringVar(&parameters.CertFile, "tlsCertFile", "/etc/webhook/certs/cert.pem", "File containing the x509 Certificate for HTTPS.")
flag.StringVar(&parameters.KeyFile, "tlsKeyFile", "/etc/webhook/certs/key.pem", "File containing the x509 private key to --tlsCertFile.")
flag.Parse()
pair, err := tls.LoadX509KeyPair(parameters.CertFile, parameters.KeyFile)
if err != nil {
glog.Errorf("Failed to load key pair: %v", err)
os.Exit(1)
}
connectHost, present := os.LookupEnv(connectHostEnv)
if !present || connectHost == "" {
glog.Error("Connect host not set")
}
connectTokenName, present := os.LookupEnv(connectTokenSecretNameEnv)
if !present || connectTokenName == "" {
glog.Error("Connect token name not set")
}
connectTokenKey, present := os.LookupEnv(connectTokenSecretKeyEnv)
if !present || connectTokenKey == "" {
glog.Error("Connect token key not set")
}
webhookConfig := webhook.Config{
ConnectHost: connectHost,
ConnectTokenName: connectTokenName,
ConnectTokenKey: connectTokenKey,
}
secretInjector := &webhook.SecretInjector{
Config: webhookConfig,
Server: &http.Server{
Addr: fmt.Sprintf(":%v", parameters.Port),
TLSConfig: &tls.Config{Certificates: []tls.Certificate{pair}},
},
}
// define http server and server handler
mux := http.NewServeMux()
mux.HandleFunc("/inject", secretInjector.Serve)
secretInjector.Server.Handler = mux
// start webhook server in new rountine
go func() {
if err := secretInjector.Server.ListenAndServeTLS("", ""); err != nil {
glog.Errorf("Failed to listen and serve webhook server: %v", err)
os.Exit(1)
}
}()
// listening OS shutdown singal
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
<-signalChan
glog.Infof("Got OS shutdown signal, shutting down webhook server gracefully...")
secretInjector.Server.Shutdown(context.Background())
}

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: op-secret-injector-webhook-deployment
namespace: op-secret-injector
labels:
app: op-secret-injector
spec:
replicas: 1
selector:
matchLabels:
app: op-secret-injector
template:
metadata:
labels:
app: op-secret-injector
spec:
containers:
- name: op-secret-injector
image: local/onepassword-secrets-injector:v1.1.0
imagePullPolicy: Never
args:
- -tlsCertFile=/etc/webhook/certs/cert.pem
- -tlsKeyFile=/etc/webhook/certs/key.pem
- -alsologtostderr
- -v=4
- 2>&1
env:
- name: OP_CONNECT_HOST
value: http://onepassword-connect:8080/
- name: OP_CONNECT_TOKEN_NAME
value: onepassword-token
- name: OP_CONNECT_TOKEN_KEY
value: token
volumeMounts:
- name: webhook-certs
mountPath: /etc/webhook/certs
readOnly: true
volumes:
- name: webhook-certs
secret:
secretName: op-secret-injector-webhook-certs

View File

@@ -0,0 +1,23 @@
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: op-secret-injector-webhook-config
labels:
app: op-secret-injector
webhooks:
- name: op-secret-injector.1password
failurePolicy: Fail
clientConfig:
service:
name: op-secret-injector-webhook-service
namespace: op-secret-injector
path: "/inject"
caBundle: ${CA_BUNDLE}
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
namespaceSelector:
matchLabels:
op-secret-injection: enabled

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: op-secret-injector-webhook-service
namespace: op-secret-injector
labels:
app: op-secret-injector
spec:
ports:
- port: 443
targetPort: 8443
selector:
app: op-secret-injector

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