Compare commits

..

1 Commits

Author SHA1 Message Date
Joris Coenen
8ee4478c46 Add GoReleaser configuration for publishing docker images
Should build both an amd64 and arm64 image and combine both in a single manifest. Does require some modifications to the GitHub Actions to correctly push to DockerHub.

Used this blog post as inspiration: https://carlosbecker.com/posts/multi-platform-docker-images-goreleaser-gh-actions/
2021-04-21 13:45:49 +02:00
61 changed files with 471 additions and 1426 deletions

View File

@@ -1 +1 @@
1.1.0 v1.0.0

View File

@@ -1,36 +0,0 @@
---
name: Bug report
about: Report bugs and errors found while using the Operator.
title: ''
labels: bug
assignees: ''
---
### Your environment
<!-- Version of the Operator when the error occurred -->
Operator Version:
<!-- What version of the Connect server are you running?
You can get this information from the Integrations section in 1Password
https://start.1password.com/integrations/active
-->
Connect Server Version:
<!-- What version of Kubernetes have you deployed the operator to? -->
Kubernetes Version:
## What happened?
<!-- Describe the bug or error -->
## What did you expect to happen?
<!-- Describe what should have happened -->
## Steps to reproduce
1. <!-- Describe Steps to reproduce the issue -->
## Notes & Logs
<!-- Paste any logs here that may help with debugging.
Remember to remove any sensitive information before sharing! -->

View File

@@ -1,9 +0,0 @@
# docs: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
blank_issues_enabled: true
contact_links:
- name: 1Password Community
url: https://1password.community/categories/secrets-automation
about: Please ask general Secrets Automation questions here.
- name: 1Password Security Bug Bounty
url: https://bugcrowd.com/agilebits
about: Please report security vulnerabilities here.

View File

@@ -1,32 +0,0 @@
---
name: Feature request
about: Suggest an idea for the Operator
title: ''
labels: feature-request
assignees: ''
---
### Summary
<!-- Briefly describe the feature in one or two sentences. You can include more details later. -->
### Use cases
<!-- Describe the use cases that make this feature useful to others.
The description should help the reader understand why the feature is necessary.
The better we understand your use case, the better we can help create an appropriate solution. -->
### Proposed solution
<!-- If you already have an idea for how the feature should work, use this space to describe it.
We'll work with you to find a workable approach, and any implementation details are appreciated.
-->
### Is there a workaround to accomplish this today?
<!-- If there's a way to accomplish this feature request without changes to the codebase, we'd like to hear it.
-->
### References & Prior Work
<!-- If a similar feature was implemented in another project or tool, add a link so we can better understand your request.
Links to relevant documentation or RFCs are also appreciated. -->
* <!-- Reference 1 -->
* <!-- Reference 2, etc -->

View File

@@ -1,15 +1,13 @@
name: release name: goreleaser
on: on:
push: push:
tags: tags:
- 'v*' - '*'
jobs: jobs:
release-docker: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
steps: steps:
- -
name: Checkout name: Checkout
@@ -17,41 +15,15 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- -
name: Docker meta name: Set up Go
id: meta uses: actions/setup-go@v2
uses: crazy-max/ghaction-docker-meta@v2
with: with:
images: | go-version: 1.15
1password/onepassword-operator
# Publish image for x.y.z and x.y
# The latest tag is automatically added for semver tags
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Get the version from tag
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
- -
name: Set up QEMU name: Run GoReleaser
uses: docker/setup-qemu-action@v1 uses: goreleaser/goreleaser-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Docker Login
uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} version: latest
password: ${{ secrets.DOCKERHUB_TOKEN }} args: release --rm-dist
- env:
name: Build and push GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: docker/build-push-action@v2
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
operator_version=${{ steps.get_version.outputs.VERSION }}

55
.goreleaser.yml Normal file
View File

@@ -0,0 +1,55 @@
project_name: onepassword-operator
builds:
- env:
- CGO_ENABLED=0
binary: manager
main: ./cmd/manager/main.go
flags:
- -mod=vendor
- -trimpath
ldflags:
- -s -w -X "github.com/1Password/onepassword-operator/version.Version={{ .Version }}"
mod_timestamp: '{{ .CommitTimestamp }}'
goos:
- linux
goarch:
- amd64
- arm64
dockers:
- image_templates: ["1password/{{ .ProjectName }}:{{ .Version }}-amd64"]
goos: linux
goarch: amd64
dockerfile: Dockerfile-goreleaser
use_buildx: true
extra_files:
- deploy/connect/
build_flag_templates:
- --platform=linux/amd64
- --label=org.opencontainers.image.title={{ .ProjectName }}
- --label=org.opencontainers.image.description={{ .ProjectName }}
- --label=org.opencontainers.image.url=https://github.com/1Password/onepassword-operator
- --label=org.opencontainers.image.source=https://github.com/1Password/onepassword-operator
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=MIT
- image_templates: ["1password/{{ .ProjectName }}:{{ .Version }}-arm64v8"]
goos: linux
goarch: arm64
dockerfile: Dockerfile-goreleaser
use_buildx: true
extra_files:
- deploy/connect/
build_flag_templates:
- --platform=linux/arm64/v8
- --label=org.opencontainers.image.title={{ .ProjectName }}
- --label=org.opencontainers.image.description={{ .ProjectName }}
- --label=org.opencontainers.image.url=https://github.com/1Password/onepassword-operator
- --label=org.opencontainers.image.source=https://github.com/1Password/onepassword-operator
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=MIT
docker_manifests:
- name_template: 1password/{{ .ProjectName }}:{{ .Version }}
image_templates:
- 1password/{{ .ProjectName }}:{{ .Version }}-amd64
- 1password/{{ .ProjectName }}:{{ .Version }}-arm64v8

View File

@@ -12,31 +12,6 @@
--- ---
[//]: # (START/v1.1.0)
# v1.1.0
## Fixes
* Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
---
[//]: # (START/v1.0.2)
# v1.0.2
## Fixes
* Name normalizer added to handle non-conforming item names.
---
[//]: # (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.
---
[//]: # (START/v1.0.0) [//]: # (START/v1.0.0)
# v1.0.0 # v1.0.0

View File

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

9
Dockerfile-goreleaser Normal file
View File

@@ -0,0 +1,9 @@
# 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 ./manager .
USER nonroot:nonroot
COPY deploy/connect/ deploy/connect/
ENTRYPOINT ["/manager"]

View File

@@ -19,17 +19,17 @@ test: ## Run test suite
test/coverage: ## Run test suite with coverage report test/coverage: ## Run test suite with coverage report
go test -v ./... -cover go test -v ./... -cover
build/operator: ## Build operator Docker image build: ## Build operator Docker image
@docker build -f operator/Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG) . @docker build -f Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG)
@echo "Successfully built and tagged image." @echo "Successfully built and tagged image."
@echo "Tag: $(DOCKER_IMG_TAG)" @echo "Tag: $(DOCKER_IMG_TAG)"
build/operator/local: ## Build local version of the operator Docker image build/local: ## Build local version of the operator Docker image
@docker build -f operator/Dockerfile -t local/$(DOCKER_IMG_TAG) . @docker build -f Dockerfile -t local/$(DOCKER_IMG_TAG)
build/operator/binary: clean ## Build operator binary build/binary: clean ## Build operator binary
@mkdir -p dist @mkdir -p dist
@go build -mod vendor -a -o manager ./operator/cmd/manager/main.go @go build -mod vendor -a -o manager ./cmd/manager/main.go
@mv manager ./dist @mv manager ./dist
clean: clean:

221
README.md
View File

@@ -1,8 +1,4 @@
# 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. 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.
@@ -10,13 +6,222 @@ The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to
The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted. The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted.
[Click here for more details on the 1Password Kubernetes Operator](operator/README.md) ## 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.b5dev.com/cs/connect)
- [1Password Connect deployed to Kubernetes](https://support.b5dev.com/cs/connect-deploy-kubernetes/#step-2-deploy-a-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
# Security #### 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 op-operator-connect-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 op-operator-connect-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/cs/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:
- **WATCH_NAMESPACE:** comma separated list of what Namespaces to watch for changes.
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect.
- **POLLING_INTERVAL** (default: 600)**:** The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
- **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace.
- **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 # {insert_new_name}
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. Furthermore, titles that include white space characters cannot be used.
---
### 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"
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"
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
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. 1Password requests you practice responsible disclosure if you discover a vulnerability.
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits). Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits). For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits).

15
build/Dockerfile Normal file
View File

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

3
build/bin/entrypoint Executable file
View File

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

11
build/bin/user_setup Executable file
View File

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

View File

@@ -11,16 +11,16 @@ import (
"strings" "strings"
"time" "time"
"github.com/1Password/onepassword-operator/operator/pkg/controller" "github.com/1Password/onepassword-operator/pkg/controller"
op "github.com/1Password/onepassword-operator/operator/pkg/onepassword" op "github.com/1Password/onepassword-operator/pkg/onepassword"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"github.com/1Password/onepassword-operator/operator/pkg/apis" "github.com/1Password/onepassword-operator/pkg/apis"
"github.com/1Password/onepassword-operator/operator/version" "github.com/1Password/onepassword-operator/version"
"github.com/1Password/connect-sdk-go/connect" "github.com/1Password/connect-sdk-go/connect"
@@ -83,11 +83,9 @@ func main() {
printVersion() printVersion()
namespace := os.Getenv(k8sutil.WatchNamespaceEnvVar) namespace, err := k8sutil.GetWatchNamespace()
deploymentNamespace, err := k8sutil.GetOperatorNamespace()
if err != nil { if err != nil {
log.Error(err, "Failed to get namespace") log.Error(err, "Failed to get watch namespace")
os.Exit(1) os.Exit(1)
} }
@@ -141,7 +139,7 @@ func main() {
go func() { go func() {
connectStarted := false connectStarted := false
for connectStarted == false { for connectStarted == false {
err := op.SetupConnect(mgr.GetClient(), deploymentNamespace) err := op.SetupConnect(mgr.GetClient())
// Cache Not Started is an acceptable error. Retry until cache is started. // Cache Not Started is an acceptable error. Retry until cache is started.
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
log.Error(err, "") log.Error(err, "")

View File

@@ -2,6 +2,7 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: onepassword-connect name: onepassword-connect
namespace: default
spec: spec:
selector: selector:
matchLabels: matchLabels:

View File

@@ -2,6 +2,7 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: onepassword-connect name: onepassword-connect
namespace: default
spec: spec:
type: NodePort type: NodePort
selector: selector:

View File

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

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

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

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

@@ -1,7 +1,7 @@
package apis package apis
import ( import (
v1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1" v1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
) )
func init() { func init() {

View File

@@ -1,7 +1,7 @@
package controller package controller
import ( import (
"github.com/1Password/onepassword-operator/operator/pkg/controller/deployment" "github.com/1Password/onepassword-operator/pkg/controller/deployment"
) )
func init() { func init() {

View File

@@ -1,7 +1,7 @@
package controller package controller
import ( import (
"github.com/1Password/onepassword-operator/operator/pkg/controller/onepassworditem" "github.com/1Password/onepassword-operator/pkg/controller/onepassworditem"
) )
func init() { func init() {

View File

@@ -3,12 +3,10 @@ package deployment
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
kubeSecrets "github.com/1Password/onepassword-operator/operator/pkg/kubernetessecrets" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/operator/pkg/onepassword" op "github.com/1Password/onepassword-operator/pkg/onepassword"
op "github.com/1Password/onepassword-operator/operator/pkg/onepassword" "github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/onepassword-operator/operator/pkg/utils"
"regexp" "regexp"
@@ -116,7 +114,7 @@ func (r *ReconcileDeployment) Reconcile(request reconcile.Request) (reconcile.Re
} }
} }
// Handles creation or updating secrets for deployment if needed // Handles creation or updating secrets for deployment if needed
if err := r.HandleApplyingDeployment(deployment, annotations, request); err != nil { if err := r.HandleApplyingDeployment(deployment.Namespace, annotations, request); err != nil {
return reconcile.Result{}, err return reconcile.Result{}, err
} }
return reconcile.Result{}, nil return reconcile.Result{}, nil
@@ -189,19 +187,10 @@ func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deploymen
return r.kubeClient.Update(context.Background(), deployment) return r.kubeClient.Update(context.Background(), deployment)
} }
func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deployment, annotations map[string]string, request reconcile.Request) error { func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotations map[string]string, request reconcile.Request) error {
reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) 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] secretName := annotations[op.NameAnnotation]
secretLabels := map[string]string(nil)
if len(secretName) == 0 { 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.") reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.")
return nil return nil
@@ -212,5 +201,5 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deploy
return fmt.Errorf("Failed to retrieve item: %v", err) return fmt.Errorf("Failed to retrieve item: %v", err)
} }
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, annotations) return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation])
} }

View File

@@ -6,8 +6,8 @@ import (
"regexp" "regexp"
"testing" "testing"
"github.com/1Password/onepassword-operator/operator/pkg/mocks" "github.com/1Password/onepassword-operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/operator/pkg/onepassword" op "github.com/1Password/onepassword-operator/pkg/onepassword"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -258,7 +258,7 @@ var tests = []testReconcileItem{
}, },
}, },
{ {
testName: "Test Do not update if Annotations have not changed", testName: "Test Do not update if OnePassword Item Version has not changed",
deploymentResource: &appsv1.Deployment{ deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: deploymentKind, Kind: deploymentKind,
@@ -271,7 +271,6 @@ var tests = []testReconcileItem{
op.ItemPathAnnotation: itemPath, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
Labels: map[string]string{},
}, },
}, },
existingSecret: &corev1.Secret{ existingSecret: &corev1.Secret{
@@ -280,8 +279,6 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -293,10 +290,7 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
}, },
Labels: map[string]string(nil),
}, },
Data: expectedSecretData, Data: expectedSecretData,
}, },

View File

@@ -4,11 +4,11 @@ import (
"context" "context"
"fmt" "fmt"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1" onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
kubeSecrets "github.com/1Password/onepassword-operator/operator/pkg/kubernetessecrets" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/operator/pkg/onepassword" "github.com/1Password/onepassword-operator/pkg/onepassword"
op "github.com/1Password/onepassword-operator/operator/pkg/onepassword" op "github.com/1Password/onepassword-operator/pkg/onepassword"
"github.com/1Password/onepassword-operator/operator/pkg/utils" "github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/connect-sdk-go/connect" "github.com/1Password/connect-sdk-go/connect"
@@ -144,21 +144,12 @@ func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem
func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error { func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
secretName := resource.GetName() secretName := resource.GetName()
labels := resource.Labels autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
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) item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath)
if err != nil { if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err) return fmt.Errorf("Failed to retrieve item: %v", err)
} }
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, annotations) return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart)
} }

View File

@@ -5,10 +5,10 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/1Password/onepassword-operator/operator/pkg/mocks" "github.com/1Password/onepassword-operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/operator/pkg/onepassword" op "github.com/1Password/onepassword-operator/pkg/onepassword"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1" onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -31,9 +31,6 @@ const (
itemId = "nwrhuano7bcwddcviubpp4mhfq" itemId = "nwrhuano7bcwddcviubpp4mhfq"
username = "test-user" username = "test-user"
password = "QmHumKc$mUeEem7caHtbaBaJ" password = "QmHumKc$mUeEem7caHtbaBaJ"
firstHost = "http://localhost:8080"
awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
iceCream = "freezing blue 20%"
userKey = "username" userKey = "username"
passKey = "password" passKey = "password"
version = 123 version = 123
@@ -119,8 +116,7 @@ var tests = []testReconcileItem{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -131,8 +127,7 @@ var tests = []testReconcileItem{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -152,11 +147,6 @@ var tests = []testReconcileItem{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
}, },
Spec: onepasswordv1.OnePasswordItemSpec{ Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath, ItemPath: itemPath,
@@ -167,10 +157,8 @@ var tests = []testReconcileItem{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: "456", op.VersionAnnotation: "456",
op.ItemPathAnnotation: itemPath,
}, },
Labels: map[string]string{},
}, },
Data: expectedSecretData, Data: expectedSecretData,
}, },
@@ -180,10 +168,8 @@ var tests = []testReconcileItem{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
}, },
Labels: map[string]string{},
}, },
Data: expectedSecretData, Data: expectedSecretData,
}, },
@@ -224,120 +210,6 @@ var tests = []testReconcileItem{
passKey: password, 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) { func TestReconcileOnePasswordItem(t *testing.T) {
@@ -369,10 +241,7 @@ func TestReconcileOnePasswordItem(t *testing.T) {
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{} item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{} item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
for k, v := range testData.opItem {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = version item.Version = version
item.Vault.ID = vaultUUID item.Vault.ID = vaultUUID
item.ID = uuid item.ID = uuid
@@ -388,8 +257,8 @@ func TestReconcileOnePasswordItem(t *testing.T) {
// watched resource . // watched resource .
req := reconcile.Request{ req := reconcile.Request{
NamespacedName: types.NamespacedName{ NamespacedName: types.NamespacedName{
Name: testData.customResource.ObjectMeta.Name, Name: name,
Namespace: testData.customResource.ObjectMeta.Namespace, Namespace: namespace,
}, },
} }
_, err := r.Reconcile(req) _, err := r.Reconcile(req)

View File

@@ -4,18 +4,12 @@ import (
"context" "context"
"fmt" "fmt"
"regexp"
"reflect"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/operator/pkg/utils" "github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
) )
@@ -29,27 +23,22 @@ const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
var log = logf.Log var log = logf.Log
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretAnnotations map[string]string) error { func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string) error {
itemVersion := fmt.Sprint(item.Version) itemVersion := fmt.Sprint(item.Version)
annotations := map[string]string{
// If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map VersionAnnotation: itemVersion,
if secretAnnotations == nil { ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID),
secretAnnotations = map[string]string{}
} }
secretAnnotations[VersionAnnotation] = itemVersion
secretAnnotations[ItemPathAnnotation] = fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
if autoRestart != "" { if autoRestart != "" {
_, err := utils.StringToBool(autoRestart) _, err := utils.StringToBool(autoRestart)
if err != nil { if err != nil {
log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName)
return err return err
} }
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart annotations[RestartDeploymentsAnnotation] = autoRestart
} }
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, *item) secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, annotations, *item)
currentSecret := &corev1.Secret{} currentSecret := &corev1.Secret{}
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
@@ -60,10 +49,9 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return err return err
} }
if !reflect.DeepEqual(currentSecret.Annotations, secretAnnotations) || !reflect.DeepEqual(currentSecret.Labels, labels) { if currentSecret.Annotations[VersionAnnotation] != itemVersion {
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
currentSecret.ObjectMeta.Annotations = secretAnnotations currentSecret.ObjectMeta.Annotations = annotations
currentSecret.ObjectMeta.Labels = labels
currentSecret.Data = secret.Data currentSecret.Data = secret.Data
return kubeClient.Update(context.Background(), currentSecret) return kubeClient.Update(context.Background(), currentSecret)
} }
@@ -72,13 +60,12 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return nil return nil
} }
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, item onepassword.Item) *corev1.Secret { func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, item onepassword.Item) *corev1.Secret {
return &corev1.Secret{ return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: utils.FormatSecretName(name), Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: annotations, Annotations: annotations,
Labels: labels,
}, },
Data: BuildKubernetesSecretData(item.Fields), Data: BuildKubernetesSecretData(item.Fields),
} }
@@ -88,34 +75,8 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byt
secretData := map[string][]byte{} secretData := map[string][]byte{}
for i := 0; i < len(fields); i++ { for i := 0; i < len(fields); i++ {
if fields[i].Value != "" { if fields[i].Value != "" {
key := formatSecretDataName(fields[i].Label) secretData[fields[i].Label] = []byte(fields[i].Value)
secretData[key] = []byte(fields[i].Value)
} }
} }
return secretData return secretData
} }
// formatSecretDataName rewrites a value to be a valid Secret data key.
//
// The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.`
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
func formatSecretDataName(value string) string {
if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 {
return value
}
return createValidSecretDataName(value)
}
var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+")
var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)")
func createValidSecretDataName(value string) string {
result := invalidStartEndChars.ReplaceAllString(value, "")
result = invalidDataChars.ReplaceAllString(result, "-")
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
}
return result
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/fake"
) )
@@ -31,11 +30,7 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient() kubeClient := fake.NewFakeClient()
secretLabels := map[string]string{} err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation)
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
@@ -47,10 +42,6 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
} }
compareFields(item.Fields, createdSecret.Data, t) compareFields(item.Fields, createdSecret.Data, t)
compareAnnotationsToItem(createdSecret.Annotations, item, t) compareAnnotationsToItem(createdSecret.Annotations, item, t)
if createdSecret.Annotations["testAnnotation"] != "exists" {
t.Errorf("Expected testAnnotation to be merged with existing annotations, but wasn't.")
}
} }
func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
@@ -64,9 +55,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient() kubeClient := fake.NewFakeClient()
secretLabels := map[string]string{} err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation)
secretAnnotations := map[string]string{}
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
@@ -77,7 +66,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
newItem.Version = 456 newItem.Version = 456
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretAnnotations) err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
@@ -110,10 +99,9 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
} }
item := onepassword.Item{} item := onepassword.Item{}
item.Fields = generateFields(5) item.Fields = generateFields(5)
labels := map[string]string{}
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item) kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item)
if kubeSecret.Name != strings.ToLower(name) { if kubeSecret.Name != name {
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
} }
if kubeSecret.Namespace != namespace { if kubeSecret.Namespace != namespace {
@@ -125,45 +113,6 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
compareFields(item.Fields, kubeSecret.Data, t) compareFields(item.Fields, kubeSecret.Data, t)
} }
func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
name := "inV@l1d k8s secret%name"
expectedName := "inv-l1d-k8s-secret-name"
namespace := "someNamespace"
annotations := map[string]string{
"annotationKey": "annotationValue",
}
labels := map[string]string{}
item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{
{
Label: "label w%th invalid ch!rs-",
Value: "value1",
},
{
Label: strings.Repeat("x", kubeValidate.DNS1123SubdomainMaxLength+1),
Value: "name exceeds max length",
},
}
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item)
// Assert Secret's meta.name was fixed
if kubeSecret.Name != expectedName {
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
}
if kubeSecret.Namespace != namespace {
t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace)
}
// assert labels were fixed for each data key
for key := range kubeSecret.Data {
if !validLabel(key) {
t.Errorf("Expected valid kubernetes label, got %s", key)
}
}
}
func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) {
actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
if err != nil { if err != nil {
@@ -215,10 +164,3 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
} }
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
} }
func validLabel(v string) bool {
if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 {
return false
}
return true
}

View File

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

View File

@@ -2,7 +2,6 @@ package onepassword
import ( import (
"context" "context"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"os" "os"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
@@ -18,13 +17,13 @@ var logConnectSetup = logf.Log.WithName("ConnectSetup")
var deploymentPath = "deploy/connect/deployment.yaml" var deploymentPath = "deploy/connect/deployment.yaml"
var servicePath = "deploy/connect/service.yaml" var servicePath = "deploy/connect/service.yaml"
func SetupConnect(kubeClient client.Client, deploymentNamespace string) error { func SetupConnect(kubeClient client.Client) error {
err := setupService(kubeClient, servicePath, deploymentNamespace) err := setupService(kubeClient, servicePath)
if err != nil { if err != nil {
return err return err
} }
err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace) err = setupDeployment(kubeClient, deploymentPath)
if err != nil { if err != nil {
return err return err
} }
@@ -32,22 +31,22 @@ func SetupConnect(kubeClient client.Client, deploymentNamespace string) error {
return nil return nil
} }
func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { func setupDeployment(kubeClient client.Client, deploymentPath string) error {
existingDeployment := &appsv1.Deployment{} existingDeployment := &appsv1.Deployment{}
// check if deployment has already been created // check if deployment has already been created
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment) err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingDeployment)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") logConnectSetup.Info("No existing Connect deployment found. Creating Deployment")
return createDeployment(kubeClient, deploymentPath, deploymentNamespace) return createDeployment(kubeClient, deploymentPath)
} }
} }
return err return err
} }
func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { func createDeployment(kubeClient client.Client, deploymentPath string) error {
deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace) deployment, err := getDeploymentToCreate(deploymentPath)
if err != nil { if err != nil {
return err return err
} }
@@ -60,16 +59,12 @@ func createDeployment(kubeClient client.Client, deploymentPath string, deploymen
return nil return nil
} }
func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*appsv1.Deployment, error) { func getDeploymentToCreate(deploymentPath string) (*appsv1.Deployment, error) {
f, err := os.Open(deploymentPath) f, err := os.Open(deploymentPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
deployment := &appsv1.Deployment{ deployment := &appsv1.Deployment{}
ObjectMeta: v1.ObjectMeta{
Namespace: deploymentNamespace,
},
}
err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment)
if err != nil { if err != nil {
@@ -78,30 +73,26 @@ func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*
return deployment, nil return deployment, nil
} }
func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { func setupService(kubeClient client.Client, servicePath string) error {
existingService := &corev1.Service{} existingService := &corev1.Service{}
//check if service has already been created //check if service has already been created
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService) err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingService)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
logConnectSetup.Info("No existing Connect service found. Creating Service") logConnectSetup.Info("No existing Connect service found. Creating Service")
return createService(kubeClient, servicePath, deploymentNamespace) return createService(kubeClient, servicePath)
} }
} }
return err return err
} }
func createService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { func createService(kubeClient client.Client, servicePath string) error {
f, err := os.Open(servicePath) f, err := os.Open(servicePath)
if err != nil { if err != nil {
return err return err
} }
service := &corev1.Service{ service := &corev1.Service{}
ObjectMeta: v1.ObjectMeta{
Namespace: deploymentNamespace,
},
}
err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service) err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service)
if err != nil { if err != nil {

View File

@@ -25,7 +25,7 @@ func TestServiceSetup(t *testing.T) {
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
client := fake.NewFakeClientWithScheme(s, objs...) client := fake.NewFakeClientWithScheme(s, objs...)
err := setupService(client, "../../deploy/connect/service.yaml", defaultNamespacedName.Namespace) err := setupService(client, "../../deploy/connect/service.yaml")
if err != nil { if err != nil {
t.Errorf("Error Setting Up Connect: %v", err) t.Errorf("Error Setting Up Connect: %v", err)
@@ -50,7 +50,7 @@ func TestDeploymentSetup(t *testing.T) {
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
client := fake.NewFakeClientWithScheme(s, objs...) client := fake.NewFakeClientWithScheme(s, objs...)
err := setupDeployment(client, "../../deploy/connect/deployment.yaml", defaultNamespacedName.Namespace) err := setupDeployment(client, "../../deploy/connect/deployment.yaml")
if err != nil { if err != nil {
t.Errorf("Error Setting Up Connect: %v", err) t.Errorf("Error Setting Up Connect: %v", err)

View File

@@ -1,10 +1,6 @@
package onepassword package onepassword
import ( import corev1 "k8s.io/api/core/v1"
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"
)
func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool { func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool {
for i := 0; i < len(containers); i++ { for i := 0; i < len(containers); i++ {
@@ -35,29 +31,3 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st
} }
return updatedDeploymentSecrets 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

@@ -1,9 +1,6 @@
package onepassword package onepassword
import ( import (
"strings"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
) )
@@ -27,14 +24,3 @@ func GetUpdatedSecretsForDeployment(deployment *appsv1.Deployment, secrets map[s
return updatedSecretsForDeployment 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

@@ -11,16 +11,6 @@ import (
var logger = logf.Log.WithName("retrieve_item") 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) { func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path) vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
if err != nil { if err != nil {
@@ -43,30 +33,6 @@ func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*one
return item, nil return item, nil
} }
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)
}
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) { func ParseVaultAndItemFromPath(path string) (string, string, error) {
splitPath := strings.Split(path, "/") splitPath := strings.Split(path, "/")
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {

View File

@@ -5,9 +5,8 @@ import (
"fmt" "fmt"
"time" "time"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
kubeSecrets "github.com/1Password/onepassword-operator/operator/pkg/kubernetessecrets" "github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/onepassword-operator/operator/pkg/utils"
"github.com/1Password/connect-sdk-go/connect" "github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
@@ -42,17 +41,12 @@ func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error {
return err return err
} }
updatedInjectedSecrets, err := h.updateInjectedSecrets() return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets)
if err != nil {
return err
}
return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets, updatedInjectedSecrets)
} }
func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret, updatedInjectedSecretsByNamespace map[string]map[string]*onepasswordv1.OnePasswordItem) error { func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret) error {
// No secrets to update. Exit
if len(updatedSecretsByNamespace) == 0 && len(updatedInjectedSecretsByNamespace) == 0 { if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil {
return nil return nil
} }
@@ -69,7 +63,6 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap() setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap()
if err != nil { if err != nil {
log.Error(err, "Error determining which namespaces allow restarts")
return err return err
} }
@@ -77,23 +70,16 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
deployment := &deployments.Items[i] deployment := &deployments.Items[i]
updatedSecrets := updatedSecretsByNamespace[deployment.Namespace] updatedSecrets := updatedSecretsByNamespace[deployment.Namespace]
// check if deployment is using one of the updated secrets
updatedDeploymentSecrets := GetUpdatedSecretsForDeployment(deployment, updatedSecrets) updatedDeploymentSecrets := GetUpdatedSecretsForDeployment(deployment, updatedSecrets)
if len(updatedDeploymentSecrets) != 0 { 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 continue
} }
for _, secret := range updatedDeploymentSecrets {
if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) {
h.restartDeployment(deployment)
continue
}
}
log.Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace)) log.Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace))
@@ -103,10 +89,9 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) { 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)) log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace))
if deployment.Spec.Template.Annotations == nil { deployment.Spec.Template.Annotations = map[string]string{
deployment.Spec.Template.Annotations = map[string]string{} RestartAnnotation: time.Now().String(),
} }
deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String()
err := h.client.Update(context.Background(), deployment) err := h.client.Update(context.Background(), deployment)
if err != nil { if err != nil {
log.Error(err, "Problem restarting deployment") log.Error(err, "Problem restarting deployment")
@@ -146,7 +131,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
} }
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
secret.Annotations[VersionAnnotation] = itemVersion secret.Annotations[VersionAnnotation] = itemVersion
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, *item) updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item)
h.client.Update(context.Background(), updatedSecret) h.client.Update(context.Background(), updatedSecret)
if updatedSecrets[secret.Namespace] == nil { if updatedSecrets[secret.Namespace] == nil {
updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret) updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)
@@ -157,52 +142,6 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
return updatedSecrets, nil 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 { func isItemLockedForForcedRestarts(item *onepassword.Item) bool {
tags := item.Tags tags := item.Tags
for i := 0; i < len(tags); i++ { for i := 0; i < len(tags); i++ {
@@ -239,7 +178,7 @@ func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string
func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
restartDeployment := secret.Annotations[RestartDeploymentsAnnotation] restartDeployment := secret.Annotations[RestartDeploymentsAnnotation]
//If annotation for auto restarts for deployment is not set. Check for the annotation on its deployment //If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
if restartDeployment == "" { if restartDeployment == "" {
return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace) return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace)
} }

View File

@@ -5,10 +5,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/1Password/onepassword-operator/operator/pkg/mocks" "github.com/1Password/onepassword-operator/pkg/mocks"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@@ -21,25 +20,23 @@ import (
) )
const ( const (
deploymentKind = "Deployment" deploymentKind = "Deployment"
deploymentAPIVersion = "v1" deploymentAPIVersion = "v1"
name = "test-deployment" name = "test-deployment"
namespace = "default" namespace = "default"
vaultId = "hfnjvi6aymbsnfc2xeeoheizda" vaultId = "hfnjvi6aymbsnfc2xeeoheizda"
itemId = "nwrhuano7bcwddcviubpp4mhfq" itemId = "nwrhuano7bcwddcviubpp4mhfq"
username = "test-user" username = "test-user"
password = "QmHumKc$mUeEem7caHtbaBaJ" password = "QmHumKc$mUeEem7caHtbaBaJ"
userKey = "username" userKey = "username"
passKey = "password" passKey = "password"
itemVersion = 123 itemVersion = 123
injectedOnePasswordItemName = "injectedsecret-" + vaultId + "-" + itemId
) )
type testUpdateSecretTask struct { type testUpdateSecretTask struct {
testName string testName string
existingDeployment *appsv1.Deployment existingDeployment *appsv1.Deployment
existingNamespace *corev1.Namespace existingNamespace *corev1.Namespace
existingOnePasswordItem *onepasswordv1.OnePasswordItem
existingSecret *corev1.Secret existingSecret *corev1.Secret
expectedError error expectedError error
expectedResultSecret *corev1.Secret expectedResultSecret *corev1.Secret
@@ -758,123 +755,6 @@ var tests = []testUpdateSecretTask{
expectedRestart: true, expectedRestart: true,
globalAutoRestartEnabled: false, 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) { func TestUpdateSecretHandler(t *testing.T) {
@@ -883,7 +763,7 @@ func TestUpdateSecretHandler(t *testing.T) {
// Register operator types with the runtime scheme. // Register operator types with the runtime scheme.
s := scheme.Scheme s := scheme.Scheme
s.AddKnownTypes(appsv1.SchemeGroupVersion, &onepasswordv1.OnePasswordItem{}, &onepasswordv1.OnePasswordItemList{}, &appsv1.Deployment{}) s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.existingDeployment)
// Objects to track in the fake client. // Objects to track in the fake client.
objs := []runtime.Object{ objs := []runtime.Object{
@@ -895,10 +775,6 @@ func TestUpdateSecretHandler(t *testing.T) {
objs = append(objs, testData.existingSecret) objs = append(objs, testData.existingSecret)
} }
if testData.existingOnePasswordItem != nil {
objs = append(objs, testData.existingOnePasswordItem)
}
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
cl := fake.NewFakeClientWithScheme(s, objs...) cl := fake.NewFakeClientWithScheme(s, objs...)
@@ -949,9 +825,9 @@ func TestUpdateSecretHandler(t *testing.T) {
_, ok := deployment.Spec.Template.Annotations[RestartAnnotation] _, ok := deployment.Spec.Template.Annotations[RestartAnnotation]
if ok { if ok {
assert.True(t, testData.expectedRestart, "Deployment was restarted but should not have been.") assert.True(t, testData.expectedRestart, "Expected deployment to restart but it did not")
} else { } else {
assert.False(t, testData.expectedRestart, "Expected deployment to restart but it did not") assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.")
} }
}) })
} }

33
pkg/utils/string.go Normal file
View File

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