Merge pull request #198 from 1Password/vzt/service-accounts-support

Service accounts support
This commit is contained in:
Volodymyr Zotov
2025-06-24 14:21:09 -05:00
committed by GitHub
32 changed files with 1725 additions and 483 deletions

View File

@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:1.21 as builder
FROM golang:1.24 as builder
ARG TARGETOS
ARG TARGETARCH
@@ -8,13 +8,15 @@ WORKDIR /workspace
COPY go.mod go.mod
COPY go.sum go.sum
# Download dependencies
RUN go mod download
# Copy the go source
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/controller/ internal/controller/
COPY pkg/ pkg/
COPY version/ version/
COPY vendor/ vendor/
# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
@@ -25,7 +27,6 @@ RUN CGO_ENABLED=0 \
GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build \
-ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \
-mod vendor \
-a -o manager cmd/main.go
# Use distroless as minimal base image to package the manager binary

View File

@@ -5,107 +5,53 @@
## Table of Contents
- [Prerequisites](#prerequisites)
- [Deploying 1Password Connect to Kubernetes](#deploying-1password-connect-to-kubernetes)
- [Kubernetes Operator Deployment](#kubernetes-operator-deployment)
- [Usage](#usage)
- [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
- [Development](#development)
1. [Configuration Options](#configuration-options)
2. [Use Kubernetes Operator with Service Account](#use-kubernetes-operator-with-service-account)
- [Create a Service Account](#1-create-a-service-account)
- [Create a Kubernetes secret](#2-create-a-kubernetes-secret-for-the-service-account)
- [Deploy the Operator](#3-deploy-the-operator)
3. [Use Kubernetes Operator with Connect](#use-kubernetes-operator-with-connect)
- [Deploy with Helm](#1-deploy-with-helm)
- [Deploy manually](#2-deploy-manually)
4. [Logging level](#logging-level)
5. [Usage examples](#usage-examples)
6. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets)
7. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
8. [Development](#development)
## 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/)
- [A `1password-credentials.json` file generated and a 1Password Connect API Token issued for the K8s Operator integration](https://developer.1password.com/docs/connect/get-started/#step-1-set-up-a-secrets-automation-workflow)
---
## Deploying 1Password Connect to Kubernetes
## Configuration options
There are 2 ways 1Password Operator can talk to 1Password servers:
- [1Password Service Accounts](https://developer.1password.com/docs/service-accounts)
- [1Password Connect](https://developer.1password.com/docs/connect/)
If 1Password Connect is already running, you can skip this step.
---
There are options to deploy 1Password Connect:
## Use Kubernetes Operator with Service Account
- [Deploy with Helm](#deploy-with-helm)
- [Deploy using the Connect Operator](#deploy-using-the-connect-operator)
### Deploy with Helm
The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes.
[The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts)
### Deploy using the Connect Operator
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`:
### 1. [Create a service account](https://developer.1password.com/docs/service-accounts/get-started#create-a-service-account)
### 2. Create a Kubernetes secret for the Service Account
- Set `OP_SERVICE_ACCOUNT_TOKEN` environment variable to the service account token you created in the previous step. This token will be used by the operator to access 1Password items.
- Create Kubernetes secret:
```bash
cat 1password-credentials.json | base64 | \
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
kubectl create secret generic onepassword-service-account-token --from-literal=token="$OP_SERVICE_ACCOUNT_TOKEN"
```
Create a Kubernetes secret from the op-session file:
```bash
kubectl create secret generic op-credentials --from-file=op-session
```
Add the following environment variable to the onepassword-connect-operator container in `/config/manager/manager.yaml`:
```yaml
- name: MANAGE_CONNECT
value: "true"
```
Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the current namespace.
### Kubernetes Operator Deployment
#### Create Kubernetes Secret for OP_CONNECT_TOKEN ####
Create a Connect token for the operator and save it as a Kubernetes Secret:
```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>)
```
**Deploying the Operator**
### 3. Deploy the Operator
An sample Deployment yaml can be found at `/config/manager/manager.yaml`.
To use Operator with Service Account, you need to set the `OP_SERVICE_ACCOUNT_TOKEN` environment variable in the `/config/manager/manager.yaml`. And remove `OP_CONNECT_TOKEN` and `OP_CONNECT_HOST` environment variables.
To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml:
- **OP_CONNECT_HOST** *(required)*: Specifies the host name within Kubernetes in which to access the 1Password Connect.
- **OP_SERVICE_ACCOUNT_TOKEN** *(required)*: Specifies Service Account token within Kubernetes to access the 1Password items.
- **WATCH_NAMESPACE:** *(default: watch all namespaces)*: Comma separated list of what Namespaces to watch for changes.
- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
- **MANAGE_CONNECT** *(default: false)*: If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the current namespace.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
You can also set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. (Note: the default value is `debug`.)
Example:
```yaml
.
.
.
containers:
- command:
- /manager
args:
- --leader-elect
- --zap-log-level=info
image: 1password/onepassword-operator:latest
.
.
.
```
To deploy the operator, simply run the following command:
```shell
@@ -118,59 +64,57 @@ make deploy
make undeploy
```
## Usage
---
To create a Kubernetes Secret from a 1Password item, create a yaml file with the following
## Use Kubernetes Operator with Connect
### 1. [Deploy with Helm](https://developer.1password.com/docs/k8s/operator/?deployment-type=helm#helm-step-1)
### 2. [Deploy manually](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#manual-step-1)
To further configure the 1Password Kubernetes Operator the following Environment variables can be set in the operator yaml:
- **OP_CONNECT_HOST** *(required)*: Specifies the host name within Kubernetes in which to access the 1Password Connect.
- **WATCH_NAMESPACE:** *(default: watch all namespaces)*: Comma separated list of what Namespaces to watch for changes.
- **POLLING_INTERVAL** *(default: 600)*: The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
- **MANAGE_CONNECT** *(default: false)*: If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the current namespace.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
---
## Logging level
You can set the logging level by setting `--zap-log-level` as an arg on the containers to either `debug`, `info` or `error`. The default value is `debug`.
Example:
```yaml
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>"
....
containers:
- command:
- /manager
args:
- --leader-elect
- --zap-log-level=info
image: 1password/onepassword-operator:latest
....
```
Deploy the OnePasswordItem to Kubernetes:
---
```bash
kubectl apply -f <your_item>.yaml
```
## Usage examples
Find usage [examples](https://developer.1password.com/docs/k8s/operator/?deployment-type=manual#usage-examples) on 1Password developer documentation.
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.
## How 1Password Items Map to Kubernetes Secrets
The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password.
In case of fields that store files, the file's contents will be used as the value.
Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence.
**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.
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.
@@ -237,6 +181,8 @@ metadata:
If the value is not set, the auto restart settings on the deployment will be used.
---
## Development
### How it works

View File

@@ -35,8 +35,6 @@ import (
"strings"
"time"
"github.com/1Password/connect-sdk-go/connect"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -54,6 +52,7 @@ import (
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/internal/controller"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/onepassword-operator/version"
//+kubebuilder:scaffold:imports
@@ -153,16 +152,19 @@ func main() {
}
// Setup One Password Client
opConnectClient, err := connect.NewClientFromEnvironment()
opClient, err := opclient.NewFromEnvironment(opclient.Config{
Logger: setupLog,
Version: version.OperatorVersion,
})
if err != nil {
setupLog.Error(err, "unable to create Connect client")
setupLog.Error(err, "unable to create 1Password client")
os.Exit(1)
}
if err = (&controller.OnePasswordItemReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
OpConnectClient: opConnectClient,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
OpClient: opClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem")
os.Exit(1)
@@ -172,7 +174,7 @@ func main() {
if err = (&controller.DeploymentReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
OpConnectClient: opConnectClient,
OpClient: opClient,
OpAnnotationRegExp: r,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Deployment")
@@ -202,7 +204,7 @@ func main() {
}
// Setup update secrets task
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opConnectClient, shouldAutoRestartDeployments())
updatedSecretsPoller := op.NewManager(mgr.GetClient(), opClient, shouldAutoRestartDeployments())
done := make(chan bool)
ticker := time.NewTicker(getPollingIntervalForUpdatingSecrets())
go func() {

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.13.0
controller-gen.kubebuilder.io/version: v0.14.0
name: onepassworditems.onepassword.com
spec:
group: onepassword.com
@@ -20,14 +20,19 @@ spec:
description: OnePasswordItem is the Schema for the onepassworditems API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object

View File

@@ -75,25 +75,33 @@ spec:
image: 1password/onepassword-operator:latest
name: manager
env:
- name: WATCH_NAMESPACE
value: "default"
- name: OPERATOR_NAME
value: "onepassword-connect-operator"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: "onepassword-connect-operator"
- name: OP_CONNECT_HOST
value: "http://onepassword-connect:8080"
- name: WATCH_NAMESPACE
value: "default"
- name: POLLING_INTERVAL
value: "10"
- name: AUTO_RESTART
value: "false"
- name: OP_CONNECT_HOST
value: "http://onepassword-connect:8080"
- name: OP_CONNECT_TOKEN
valueFrom:
secretKeyRef:
name: onepassword-token
key: token
- name: AUTO_RESTART
- name: MANAGE_CONNECT
value: "false"
# Uncomment the following lines to enable service account token and comment out the OP_CONNECT_TOKEN, OP_CONNECT_HOST and MANAGE_CONNECT env vars.
# - name: OP_SERVICE_ACCOUNT_TOKEN
# valueFrom:
# secretKeyRef:
# name: onepassword-service-account-token
# key: token
securityContext:
allowPrivilegeEscalation: false
capabilities:
@@ -116,9 +124,9 @@ spec:
resources:
limits:
cpu: 500m
memory: 128Mi
memory: 512Mi
requests:
cpu: 10m
memory: 64Mi
cpu: 100m
memory: 128Mi
serviceAccountName: onepassword-connect-operator
terminationGracePeriodSeconds: 10

29
go.mod
View File

@@ -1,14 +1,15 @@
module github.com/1Password/onepassword-operator
go 1.21
go 1.24
toolchain go1.21.5
toolchain go1.24.4
require (
github.com/1Password/connect-sdk-go v1.5.3
github.com/1password/onepassword-sdk-go v0.3.1
github.com/onsi/ginkgo/v2 v2.14.0
github.com/onsi/gomega v1.30.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
k8s.io/api v0.29.3
k8s.io/apimachinery v0.29.3
k8s.io/client-go v0.29.3
@@ -20,16 +21,19 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/extism/go-sdk v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -38,6 +42,7 @@ require (
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -53,22 +58,26 @@ require (
github.com/prometheus/common v0.51.1 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.19.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

48
go.sum
View File

@@ -1,5 +1,7 @@
github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk=
github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs=
github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXhyj4NBmo5Tdamc=
github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -12,16 +14,20 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/extism/go-sdk v1.7.0 h1:yHbSa2JbcF60kjGsYiGEOcClfbknqCJchyh9TRibFWo=
github.com/extism/go-sdk v1.7.0/go.mod h1:Dhuc1qcD0aqjdqJ3ZDyGdkZPEj/EHKVjbE4P+1XRMqc=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
@@ -32,6 +38,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -54,6 +62,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -102,8 +112,12 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
@@ -111,6 +125,8 @@ github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -134,8 +150,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -150,18 +166,18 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -169,8 +185,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -181,8 +197,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -28,14 +28,13 @@ import (
"context"
"fmt"
"regexp"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/1Password/connect-sdk-go/connect"
"strings"
"time"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/utils"
appsv1 "k8s.io/api/apps/v1"
@@ -47,6 +46,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var logDeployment = logf.Log.WithName("controller_deployment")
@@ -55,7 +55,7 @@ var logDeployment = logf.Log.WithName("controller_deployment")
type DeploymentReconciler struct {
client.Client
Scheme *runtime.Scheme
OpConnectClient connect.Client
OpClient opclient.Client
OpAnnotationRegExp *regexp.Regexp
}
@@ -103,7 +103,12 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}
// Handles creation or updating secrets for deployment if needed
if err = r.handleApplyingDeployment(deployment, deployment.Namespace, annotations, req); err != nil {
return ctrl.Result{}, err
if strings.Contains(err.Error(), "rate limit") {
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
} else {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
@@ -196,7 +201,7 @@ func (r *DeploymentReconciler) handleApplyingDeployment(deployment *appsv1.Deplo
return nil
}
item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, annotations[op.ItemPathAnnotation])
item, err := op.GetOnePasswordItemByPath(r.OpClient, annotations[op.ItemPathAnnotation])
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}

View File

@@ -2,9 +2,6 @@ package controller
import (
"context"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
"time"
. "github.com/onsi/ginkgo/v2"
@@ -17,6 +14,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
)
const (
@@ -106,17 +104,8 @@ var _ = Describe("Deployment controller", func() {
}
mockGetItemFunc := func() {
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{}
for k, v := range item1.Data {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = item1.Version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
// mock GetItemByID to return test item 'item1'
mockGetItemByIDFunc.Return(item1.ToModel(), nil)
}
BeforeEach(func() {
@@ -151,17 +140,10 @@ var _ = Describe("Deployment controller", func() {
It("Should update existing K8s Secret using deployment", func() {
By("Updating secret")
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{}
for k, v := range item2.Data {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = item2.Version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
// mock GetItemByID to return test item 'item2'
mockGetItemByIDFunc.Return(item2.ToModel(), nil)
Eventually(func() error {
updatedDeployment := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{

View File

@@ -27,13 +27,14 @@ package controller
import (
"context"
"fmt"
"github.com/1Password/connect-sdk-go/connect"
"strings"
"time"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1"
@@ -52,8 +53,8 @@ var finalizer = "onepassword.com/finalizer.secret"
// OnePasswordItemReconciler reconciles a OnePasswordItem object
type OnePasswordItemReconciler struct {
client.Client
Scheme *runtime.Scheme
OpConnectClient connect.Client
Scheme *runtime.Scheme
OpClient opclient.Client
}
//+kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete
@@ -104,6 +105,12 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// Handles creation or updating secrets for deployment if needed
err = r.handleOnePasswordItem(onepassworditem, req)
if err != nil {
if strings.Contains(err.Error(), "rate limit") {
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
}
}
if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil {
return ctrl.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr)
}
@@ -164,7 +171,7 @@ func (r *OnePasswordItemReconciler) handleOnePasswordItem(resource *onepasswordv
secretType := resource.Type
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
item, err := op.GetOnePasswordItemByPath(r.OpConnectClient, resource.Spec.ItemPath)
item, err := op.GetOnePasswordItemByPath(r.OpClient, resource.Spec.ItemPath)
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}

View File

@@ -2,10 +2,6 @@ package controller
import (
"context"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/mocks"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -16,6 +12,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
const (
@@ -32,17 +29,8 @@ var _ = Describe("OnePasswordItem controller", func() {
err = k8sClient.DeleteAllOf(context.Background(), &v1.Secret{}, client.InNamespace(namespace))
Expect(err).ToNot(HaveOccurred())
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{}
for k, v := range item1.Data {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = item1.Version
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
item := item1.ToModel()
mockGetItemByIDFunc.Return(item, nil)
})
Context("Happy path", func() {
@@ -99,17 +87,13 @@ var _ = Describe("OnePasswordItem controller", func() {
"password": []byte("##newPassword##"),
"extraField": []byte("dev"),
}
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{}
for k, v := range newData {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = item1.Version + 1
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
item := item2.ToModel()
for k, v := range newData {
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
}
mockGetItemByIDFunc.Return(item, nil)
_, err := onePasswordItemReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})
Expect(err).ToNot(HaveOccurred())
@@ -178,18 +162,11 @@ var _ = Describe("OnePasswordItem controller", func() {
"ice-cream-type": []byte(iceCream),
}
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Title = "!my sECReT it3m%"
item.Fields = []*onepassword.ItemField{}
for k, v := range testData {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = item1.Version + 1
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
item := item2.ToModel()
for k, v := range testData {
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
}
mockGetItemByIDFunc.Return(item, nil)
By("Creating a new OnePasswordItem successfully")
Expect(k8sClient.Create(ctx, toCreate)).Should(Succeed())

View File

@@ -31,10 +31,9 @@ import (
"testing"
"time"
"github.com/1Password/onepassword-operator/pkg/mocks"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
@@ -45,6 +44,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log/zap"
onepasswordcomv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/pkg/mocks"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
//+kubebuilder:scaffold:imports
)
@@ -78,8 +79,11 @@ var (
cancel context.CancelFunc
onePasswordItemReconciler *OnePasswordItemReconciler
deploymentReconciler *DeploymentReconciler
mockGetItemByIDFunc *mock.Call
item1 = &TestItem{
ItemID: "nwrhuano7bcwddcviubpp4mhfq",
VaultID: "hfnjvi6aymbsnfc2xeeoheizda",
Name: "test-item",
Version: 123,
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizda/items/nwrhuano7bcwddcviubpp4mhfq",
@@ -94,6 +98,8 @@ var (
}
item2 = &TestItem{
ItemID: "nwrhuano7bcwddcviubpp4mhf2",
VaultID: "hfnjvi6aymbsnfc2xeeoheizd2",
Name: "test-item2",
Path: "vaults/hfnjvi6aymbsnfc2xeeoheizd2/items/nwrhuano7bcwddcviubpp4mhf2",
Version: 456,
@@ -109,6 +115,8 @@ var (
)
type TestItem struct {
ItemID string
VaultID string
Name string
Version int
Path string
@@ -116,6 +124,20 @@ type TestItem struct {
SecretData map[string][]byte
}
func (ti *TestItem) ToModel() *model.Item {
item := &model.Item{}
item.Version = ti.Version
item.VaultID = ti.VaultID
item.ID = ti.ItemID
item.Fields = []model.ItemField{}
for k, v := range ti.Data {
item.Fields = append(item.Fields, model.ItemField{Label: k, Value: v})
}
return item
}
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
@@ -153,12 +175,13 @@ var _ = BeforeSuite(func() {
})
Expect(err).ToNot(HaveOccurred())
opConnectClient := &mocks.TestClient{}
mockOpClient := &mocks.TestClient{}
mockGetItemByIDFunc = mockOpClient.On("GetItemByID", mock.Anything, mock.Anything)
onePasswordItemReconciler = &OnePasswordItemReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
OpConnectClient: opConnectClient,
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
OpClient: mockOpClient,
}
err = (onePasswordItemReconciler).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())
@@ -167,7 +190,7 @@ var _ = BeforeSuite(func() {
deploymentReconciler = &DeploymentReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
OpConnectClient: opConnectClient,
OpClient: mockOpClient,
OpAnnotationRegExp: r,
}
err = (deploymentReconciler).SetupWithManager(k8sManager)

View File

@@ -2,17 +2,13 @@ package kubernetessecrets
import (
"context"
errs "errors"
"fmt"
"reflect"
"regexp"
"strings"
"reflect"
errs "errors"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
"github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@@ -34,11 +30,11 @@ var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type
var log = logf.Log
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error {
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *model.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error {
itemVersion := fmt.Sprint(item.Version)
secretAnnotations := map[string]string{
VersionAnnotation: itemVersion,
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID),
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID),
}
if autoRestart != "" {
@@ -92,7 +88,7 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return nil
}
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret {
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item model.Item, ownerRef *metav1.OwnerReference) *corev1.Secret {
var ownerRefs []metav1.OwnerReference
if ownerRef != nil {
ownerRefs = []metav1.OwnerReference{*ownerRef}
@@ -111,7 +107,7 @@ func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotation
}
}
func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte {
func BuildKubernetesSecretData(fields []model.ItemField, files []model.File) map[string][]byte {
secretData := map[string][]byte{}
for i := 0; i < len(fields); i++ {
if fields[i].Value != "" {

View File

@@ -3,11 +3,10 @@ package kubernetessecrets
import (
"context"
"fmt"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
"strings"
"testing"
"github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -21,10 +20,10 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
secretName := "test-secret-name"
namespace := "test"
item := onepassword.Item{}
item := model.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewClientBuilder().Build()
@@ -49,10 +48,10 @@ func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
secretName := "test-secret-name"
namespace := "test"
item := onepassword.Item{}
item := model.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewClientBuilder().Build()
@@ -94,10 +93,10 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
secretName := "test-secret-update"
namespace := "test"
item := onepassword.Item{}
item := model.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewClientBuilder().Build()
@@ -111,10 +110,10 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
}
// Updating kubernetes secret with new item
newItem := onepassword.Item{}
newItem := model.Item{}
newItem.Fields = generateFields(6)
newItem.Version = 456
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
newItem.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil)
if err != nil {
@@ -147,7 +146,7 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
annotations := map[string]string{
annotationKey: annotationValue,
}
item := onepassword.Item{}
item := model.Item{}
item.Fields = generateFields(5)
labels := map[string]string{}
secretType := ""
@@ -173,10 +172,10 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
"annotationKey": "annotationValue",
}
labels := map[string]string{}
item := onepassword.Item{}
item := model.Item{}
secretType := ""
item.Fields = []*onepassword.ItemField{
item.Fields = []model.ItemField{
{
Label: "label w%th invalid ch!rs-",
Value: "value1",
@@ -209,10 +208,10 @@ func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
secretName := "tls-test-secret-name"
namespace := "test"
item := onepassword.Item{}
item := model.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.VaultID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewClientBuilder().Build()
@@ -235,13 +234,13 @@ func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
}
}
func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) {
func compareAnnotationsToItem(annotations map[string]string, item model.Item, t *testing.T) {
actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
if err != nil {
t.Errorf("Was unable to parse Item Path")
}
if actualVaultId != item.Vault.ID {
t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId)
if actualVaultId != item.VaultID {
t.Errorf("Expected annotation vault id to be %v but was %v", item.VaultID, actualVaultId)
}
if actualItemId != item.ID {
t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId)
@@ -255,7 +254,7 @@ func compareAnnotationsToItem(annotations map[string]string, item onepassword.It
}
}
func compareFields(actualFields []*onepassword.ItemField, secretData map[string][]byte, t *testing.T) {
func compareFields(actualFields []model.ItemField, secretData map[string][]byte, t *testing.T) {
for i := 0; i < len(actualFields); i++ {
value, found := secretData[actualFields[i].Label]
if !found {
@@ -267,14 +266,13 @@ func compareFields(actualFields []*onepassword.ItemField, secretData map[string]
}
}
func generateFields(numToGenerate int) []*onepassword.ItemField {
fields := []*onepassword.ItemField{}
func generateFields(numToGenerate int) []model.ItemField {
fields := []model.ItemField{}
for i := 0; i < numToGenerate; i++ {
field := onepassword.ItemField{
fields = append(fields, model.ItemField{
Label: "key" + fmt.Sprint(i),
Value: "value" + fmt.Sprint(i),
}
fields = append(fields, &field)
})
}
return fields
}

View File

@@ -1,151 +1,37 @@
package mocks
import (
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/stretchr/testify/mock"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
type TestClient struct {
GetVaultsFunc func() ([]onepassword.Vault, error)
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
GetVaultFunc func(uuid string) (*onepassword.Vault, error)
GetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error)
GetVaultByTitleFunc func(title string) (*onepassword.Vault, error)
GetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error)
GetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error)
GetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error)
GetItemsFunc func(vaultQuery string) ([]onepassword.Item, error)
GetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error)
CreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
UpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
DeleteItemFunc func(item *onepassword.Item, vaultQuery string) error
DeleteItemByIDFunc func(itemUUID string, vaultQuery string) error
DeleteItemByTitleFunc func(title string, vaultQuery string) error
GetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error)
GetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error)
GetFileContentFunc func(file *onepassword.File) ([]byte, error)
DownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error)
LoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error
LoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error
LoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error
LoadStructFunc func(config interface{}) error
mock.Mock
}
var (
DoGetVaultsFunc func() ([]onepassword.Vault, error)
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
DoGetVaultFunc func(uuid string) (*onepassword.Vault, error)
DoGetVaultByUUIDFunc func(uuid string) (*onepassword.Vault, error)
DoGetVaultByTitleFunc func(title string) (*onepassword.Vault, error)
DoGetItemFunc func(itemQuery string, vaultQuery string) (*onepassword.Item, error)
DoGetItemByUUIDFunc func(uuid string, vaultQuery string) (*onepassword.Item, error)
DoGetItemByTitleFunc func(title string, vaultQuery string) (*onepassword.Item, error)
DoGetItemsFunc func(vaultQuery string) ([]onepassword.Item, error)
DoGetItemsByTitleFunc func(title string, vaultQuery string) ([]onepassword.Item, error)
DoCreateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
DoUpdateItemFunc func(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error)
DoDeleteItemFunc func(item *onepassword.Item, vaultQuery string) error
DoDeleteItemByIDFunc func(itemUUID string, vaultQuery string) error
DoDeleteItemByTitleFunc func(title string, vaultQuery string) error
DoGetFilesFunc func(itemQuery string, vaultQuery string) ([]onepassword.File, error)
DoGetFileFunc func(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error)
DoGetFileContentFunc func(file *onepassword.File) ([]byte, error)
DoDownloadFileFunc func(file *onepassword.File, targetDirectory string, overwrite bool) (string, error)
DoLoadStructFromItemByUUIDFunc func(config interface{}, itemUUID string, vaultQuery string) error
DoLoadStructFromItemByTitleFunc func(config interface{}, itemTitle string, vaultQuery string) error
DoLoadStructFromItemFunc func(config interface{}, itemQuery string, vaultQuery string) error
DoLoadStructFunc func(config interface{}) error
)
// Do is the mock client's `Do` func
func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
return DoGetVaultsFunc()
func (tc *TestClient) GetItemByID(vaultID, itemID string) (*model.Item, error) {
args := tc.Called(vaultID, itemID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.Item), args.Error(1)
}
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
return DoGetVaultsByTitleFunc(title)
func (tc *TestClient) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) {
args := tc.Called(vaultID, itemTitle)
return args.Get(0).([]model.Item), args.Error(1)
}
func (m *TestClient) GetVault(vaultQuery string) (*onepassword.Vault, error) {
return DoGetVaultFunc(vaultQuery)
func (tc *TestClient) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) {
args := tc.Called(vaultID, itemID, fileID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}
func (m *TestClient) GetVaultByUUID(uuid string) (*onepassword.Vault, error) {
return DoGetVaultByUUIDFunc(uuid)
}
func (m *TestClient) GetVaultByTitle(title string) (*onepassword.Vault, error) {
return DoGetVaultByTitleFunc(title)
}
func (m *TestClient) GetItem(itemQuery string, vaultQuery string) (*onepassword.Item, error) {
return DoGetItemFunc(itemQuery, vaultQuery)
}
func (m *TestClient) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) {
return DoGetItemByUUIDFunc(uuid, vaultQuery)
}
func (m *TestClient) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) {
return DoGetItemByTitleFunc(title, vaultQuery)
}
func (m *TestClient) GetItems(vaultQuery string) ([]onepassword.Item, error) {
return DoGetItemsFunc(vaultQuery)
}
func (m *TestClient) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) {
return DoGetItemsByTitleFunc(title, vaultQuery)
}
func (m *TestClient) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
return DoCreateItemFunc(item, vaultQuery)
}
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
return DoUpdateItemFunc(item, vaultQuery)
}
func (m *TestClient) DeleteItem(item *onepassword.Item, vaultQuery string) error {
return DoDeleteItemFunc(item, vaultQuery)
}
func (m *TestClient) DeleteItemByID(itemUUID string, vaultQuery string) error {
return DoDeleteItemByIDFunc(itemUUID, vaultQuery)
}
func (m *TestClient) DeleteItemByTitle(title string, vaultQuery string) error {
return DoDeleteItemByTitleFunc(title, vaultQuery)
}
func (m *TestClient) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) {
return DoGetFilesFunc(itemQuery, vaultQuery)
}
func (m *TestClient) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) {
return DoGetFileFunc(uuid, itemQuery, vaultQuery)
}
func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) {
return DoGetFileContentFunc(file)
}
func (m *TestClient) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) {
return DoDownloadFileFunc(file, targetDirectory, overwrite)
}
func (m *TestClient) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error {
return DoLoadStructFromItemByUUIDFunc(config, itemUUID, vaultQuery)
}
func (m *TestClient) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error {
return DoLoadStructFromItemByTitleFunc(config, itemTitle, vaultQuery)
}
func (m *TestClient) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error {
return DoLoadStructFromItemFunc(config, itemQuery, vaultQuery)
}
func (m *TestClient) LoadStruct(config interface{}) error {
return DoLoadStructFunc(config)
func (tc *TestClient) GetVaultsByTitle(title string) ([]model.Vault, error) {
args := tc.Called(title)
return args.Get(0).([]model.Vault), args.Error(1)
}

View File

@@ -0,0 +1,55 @@
package client
import (
"errors"
"os"
"github.com/go-logr/logr"
"github.com/1Password/onepassword-operator/pkg/onepassword/client/connect"
"github.com/1Password/onepassword-operator/pkg/onepassword/client/sdk"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
// Client is an interface for interacting with 1Password items and vaults.
type Client interface {
GetItemByID(vaultID, itemID string) (*model.Item, error)
GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error)
GetFileContent(vaultID, itemID, fileID string) ([]byte, error)
GetVaultsByTitle(title string) ([]model.Vault, error)
}
type Config struct {
Logger logr.Logger
Version string
}
// NewFromEnvironment creates a new 1Password client based on the provided configuration.
func NewFromEnvironment(cfg Config) (Client, error) {
connectHost, _ := os.LookupEnv("OP_CONNECT_HOST")
connectToken, _ := os.LookupEnv("OP_CONNECT_TOKEN")
serviceAccountToken, _ := os.LookupEnv("OP_SERVICE_ACCOUNT_TOKEN")
if connectHost != "" && connectToken != "" && serviceAccountToken != "" {
return nil, errors.New("invalid configuration. Either Connect or Service Account credentials should be set, not both")
}
if serviceAccountToken != "" {
cfg.Logger.Info("Using Service Account Token")
return sdk.NewClient(sdk.Config{
ServiceAccountToken: serviceAccountToken,
IntegrationName: "1password-operator",
IntegrationVersion: cfg.Version,
})
}
if connectHost != "" && connectToken != "" {
cfg.Logger.Info("Using 1Password Connect")
return connect.NewClient(connect.Config{
ConnectHost: connectHost,
ConnectToken: connectToken,
}), nil
}
return nil, errors.New("invalid configuration. Connect or Service Account credentials should be set")
}

View File

@@ -0,0 +1,83 @@
package connect
import (
"fmt"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
// Config holds the configuration for the Connect client.
type Config struct {
ConnectHost string
ConnectToken string
}
// Connect is a client for interacting with 1Password using the Connect API.
type Connect struct {
client connect.Client
}
// NewClient creates a new Connect client using provided configuration.
func NewClient(config Config) *Connect {
return &Connect{
client: connect.NewClient(config.ConnectHost, config.ConnectToken),
}
}
func (c *Connect) GetItemByID(vaultID, itemID string) (*model.Item, error) {
connectItem, err := c.client.GetItemByUUID(itemID, vaultID)
if err != nil {
return nil, fmt.Errorf("1Password Connect error: %w", err)
}
var item model.Item
item.FromConnectItem(connectItem)
return &item, nil
}
func (c *Connect) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) {
// Get all items in the vault with the specified title
connectItems, err := c.client.GetItemsByTitle(itemTitle, vaultID)
if err != nil {
return nil, fmt.Errorf("1Password Connect error: %w", err)
}
items := make([]model.Item, len(connectItems))
for i, connectItem := range connectItems {
var item model.Item
item.FromConnectItem(&connectItem)
items[i] = item
}
return items, nil
}
func (c *Connect) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) {
bytes, err := c.client.GetFileContent(&onepassword.File{
ContentPath: fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s/content", vaultID, itemID, fileID),
})
if err != nil {
return nil, fmt.Errorf("1Password Connect error: %w", err)
}
return bytes, nil
}
func (c *Connect) GetVaultsByTitle(vaultQuery string) ([]model.Vault, error) {
connectVaults, err := c.client.GetVaultsByTitle(vaultQuery)
if err != nil {
return nil, fmt.Errorf("1Password Connect error: %w", err)
}
var vaults []model.Vault
for _, connectVault := range connectVaults {
if vaultQuery == connectVault.Name {
var vault model.Vault
vault.FromConnectVault(&connectVault)
vaults = append(vaults, vault)
}
}
return vaults, nil
}

View File

@@ -0,0 +1,240 @@
package connect
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/1Password/connect-sdk-go/onepassword"
clienttesting "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing"
"github.com/1Password/onepassword-operator/pkg/onepassword/client/testing/mock"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
const VaultTitleEmployee = "Employee"
func TestConnect_GetItemByID(t *testing.T) {
connectItem := clienttesting.CreateConnectItem()
testCases := map[string]struct {
mockClient func() *mock.ConnectClientMock
check func(t *testing.T, item *model.Item, err error)
}{
"should return an item": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemByUUID", "item-id", "vault-id").Return(connectItem, nil)
return mockConnectClient
},
check: func(t *testing.T, item *model.Item, err error) {
require.NoError(t, err)
clienttesting.CheckConnectItemMapping(t, connectItem, item)
},
},
"should return an error": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemByUUID", "item-id", "vault-id").Return((*onepassword.Item)(nil), errors.New("error"))
return mockConnectClient
},
check: func(t *testing.T, item *model.Item, err error) {
require.Error(t, err)
require.Nil(t, item)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &Connect{client: tc.mockClient()}
item, err := client.GetItemByID("vault-id", "item-id")
tc.check(t, item, err)
})
}
}
func TestConnect_GetItemsByTitle(t *testing.T) {
connectItem1 := clienttesting.CreateConnectItem()
connectItem2 := clienttesting.CreateConnectItem()
testCases := map[string]struct {
mockClient func() *mock.ConnectClientMock
check func(t *testing.T, items []model.Item, err error)
}{
"should return a single item": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return(
[]onepassword.Item{
*connectItem1,
}, nil)
return mockConnectClient
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 1)
require.Equal(t, connectItem1.ID, items[0].ID)
},
},
"should return two items": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return(
[]onepassword.Item{
*connectItem1,
*connectItem2,
}, nil)
return mockConnectClient
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 2)
clienttesting.CheckConnectItemMapping(t, connectItem1, &items[0])
clienttesting.CheckConnectItemMapping(t, connectItem2, &items[1])
},
},
"should return an error": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetItemsByTitle", "item-title", "vault-id").Return([]onepassword.Item{}, errors.New("error"))
return mockConnectClient
},
check: func(t *testing.T, items []model.Item, err error) {
require.Error(t, err)
require.Nil(t, items)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &Connect{client: tc.mockClient()}
items, err := client.GetItemsByTitle("vault-id", "item-title")
tc.check(t, items, err)
})
}
}
func TestConnect_GetFileContent(t *testing.T) {
testCases := map[string]struct {
mockClient func() *mock.ConnectClientMock
check func(t *testing.T, content []byte, err error)
}{
"should return file content": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetFileContent", &onepassword.File{
ContentPath: "/v1/vaults/vault-id/items/item-id/files/file-id/content",
}).Return([]byte("file content"), nil)
return mockConnectClient
},
check: func(t *testing.T, content []byte, err error) {
require.NoError(t, err)
require.Equal(t, []byte("file content"), content)
},
},
"should return an error": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetFileContent", &onepassword.File{
ContentPath: "/v1/vaults/vault-id/items/item-id/files/file-id/content",
}).Return(nil, errors.New("error"))
return mockConnectClient
},
check: func(t *testing.T, content []byte, err error) {
require.Error(t, err)
require.Nil(t, content)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &Connect{client: tc.mockClient()}
content, err := client.GetFileContent("vault-id", "item-id", "file-id")
tc.check(t, content, err)
})
}
}
func TestConnect_GetVaultsByTitle(t *testing.T) {
now := time.Now()
testCases := map[string]struct {
mockClient func() *mock.ConnectClientMock
check func(t *testing.T, vaults []model.Vault, err error)
}{
"should return a single vault": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{
{
ID: "test-id",
Name: VaultTitleEmployee,
CreatedAt: now,
},
{
ID: "test-id-2",
Name: "Some other vault",
CreatedAt: now,
},
}, nil)
return mockConnectClient
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.NoError(t, err)
require.Len(t, vaults, 1)
require.Equal(t, "test-id", vaults[0].ID)
require.Equal(t, now, vaults[0].CreatedAt)
},
},
"should return a two vaults": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{
{
ID: "test-id",
Name: VaultTitleEmployee,
CreatedAt: now,
},
{
ID: "test-id-2",
Name: VaultTitleEmployee,
CreatedAt: now,
},
}, nil)
return mockConnectClient
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.NoError(t, err)
require.Len(t, vaults, 2)
// Check the first vault
require.Equal(t, "test-id", vaults[0].ID)
require.Equal(t, now, vaults[0].CreatedAt)
// Check the second vault
require.Equal(t, "test-id-2", vaults[1].ID)
require.Equal(t, now, vaults[1].CreatedAt)
},
},
"should return an error": {
mockClient: func() *mock.ConnectClientMock {
mockConnectClient := &mock.ConnectClientMock{}
mockConnectClient.On("GetVaultsByTitle", VaultTitleEmployee).Return([]onepassword.Vault{}, errors.New("error"))
return mockConnectClient
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.Error(t, err)
require.Empty(t, vaults)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &Connect{client: tc.mockClient()}
vault, err := client.GetVaultsByTitle(VaultTitleEmployee)
tc.check(t, vault, err)
})
}
}

View File

@@ -0,0 +1,97 @@
package sdk
import (
"context"
"fmt"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
sdk "github.com/1password/onepassword-sdk-go"
)
// Config holds the configuration for the 1Password SDK client.
type Config struct {
ServiceAccountToken string
IntegrationName string
IntegrationVersion string
}
// SDK is a client for interacting with 1Password using the SDK.
type SDK struct {
client *sdk.Client
}
func NewClient(config Config) (*SDK, error) {
client, err := sdk.NewClient(context.Background(),
sdk.WithServiceAccountToken(config.ServiceAccountToken),
sdk.WithIntegrationInfo(config.IntegrationName, config.IntegrationVersion),
)
if err != nil {
return nil, fmt.Errorf("1Password sdk error: %w", err)
}
return &SDK{
client: client,
}, nil
}
func (s *SDK) GetItemByID(vaultID, itemID string) (*model.Item, error) {
sdkItem, err := s.client.Items().Get(context.Background(), vaultID, itemID)
if err != nil {
return nil, fmt.Errorf("1Password sdk error: %w", err)
}
var item model.Item
item.FromSDKItem(&sdkItem)
return &item, nil
}
func (s *SDK) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) {
// Get all items in the vault
sdkItems, err := s.client.Items().List(context.Background(), vaultID)
if err != nil {
return nil, fmt.Errorf("1Password sdk error: %w", err)
}
// Filter items by title
var items []model.Item
for _, sdkItem := range sdkItems {
if sdkItem.Title == itemTitle {
var item model.Item
item.FromSDKItemOverview(&sdkItem)
items = append(items, item)
}
}
return items, nil
}
func (s *SDK) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) {
bytes, err := s.client.Items().Files().Read(context.Background(), vaultID, itemID, sdk.FileAttributes{
ID: fileID,
})
if err != nil {
return nil, fmt.Errorf("1Password sdk error: %w", err)
}
return bytes, nil
}
func (s *SDK) GetVaultsByTitle(title string) ([]model.Vault, error) {
// List all vaults
sdkVaults, err := s.client.Vaults().List(context.Background())
if err != nil {
return nil, fmt.Errorf("1Password sdk error: %w", err)
}
// Filter vaults by title
var vaults []model.Vault
for _, sdkVault := range sdkVaults {
if sdkVault.Title == title {
var vault model.Vault
vault.FromSDKVault(&sdkVault)
vaults = append(vaults, vault)
}
}
return vaults, nil
}

View File

@@ -0,0 +1,288 @@
package sdk
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
clienttesting "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing"
clientmock "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing/mock"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
sdk "github.com/1password/onepassword-sdk-go"
)
const VaultTitleEmployee = "Employee"
func TestSDK_GetItemByID(t *testing.T) {
sdkItem := clienttesting.CreateSDKItem()
testCases := map[string]struct {
mockItemAPI func() *clientmock.ItemAPIMock
check func(t *testing.T, item *model.Item, err error)
}{
"should return a single item": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("Get", context.Background(), "vault-id", "item-id").Return(*sdkItem, nil)
return m
},
check: func(t *testing.T, item *model.Item, err error) {
require.NoError(t, err)
clienttesting.CheckSDKItemMapping(t, sdkItem, item)
},
},
"should return an error": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("Get", context.Background(), "vault-id", "item-id").Return(sdk.Item{}, errors.New("error"))
return m
},
check: func(t *testing.T, item *model.Item, err error) {
require.Error(t, err)
require.Empty(t, item)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &SDK{
client: &sdk.Client{
ItemsAPI: tc.mockItemAPI(),
},
}
item, err := client.GetItemByID("vault-id", "item-id")
tc.check(t, item, err)
})
}
}
func TestSDK_GetItemsByTitle(t *testing.T) {
sdkItem1 := clienttesting.CreateSDKItemOverview()
sdkItem2 := clienttesting.CreateSDKItemOverview()
testCases := map[string]struct {
mockItemAPI func() *clientmock.ItemAPIMock
check func(t *testing.T, items []model.Item, err error)
}{
"should return a single item": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
copySDKItem2 := *sdkItem2
copySDKItem2.Title = "Some other item"
m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{
*sdkItem1,
copySDKItem2,
}, nil)
return m
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 1)
clienttesting.CheckSDKItemOverviewMapping(t, sdkItem1, &items[0])
},
},
"should return a two items": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{
*sdkItem1,
*sdkItem2,
}, nil)
return m
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 2)
clienttesting.CheckSDKItemOverviewMapping(t, sdkItem1, &items[0])
clienttesting.CheckSDKItemOverviewMapping(t, sdkItem2, &items[1])
},
},
"should return empty list": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{}, nil)
return m
},
check: func(t *testing.T, items []model.Item, err error) {
require.NoError(t, err)
require.Len(t, items, 0)
},
},
"should return an error": {
mockItemAPI: func() *clientmock.ItemAPIMock {
m := &clientmock.ItemAPIMock{}
m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{}, errors.New("error"))
return m
},
check: func(t *testing.T, items []model.Item, err error) {
require.Error(t, err)
require.Empty(t, items)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &SDK{
client: &sdk.Client{
ItemsAPI: tc.mockItemAPI(),
},
}
items, err := client.GetItemsByTitle("vault-id", "item-title")
tc.check(t, items, err)
})
}
}
func TestSDK_GetFileContent(t *testing.T) {
testCases := map[string]struct {
mockItemAPI func() *clientmock.ItemAPIMock
check func(t *testing.T, content []byte, err error)
}{
"should return file content": {
mockItemAPI: func() *clientmock.ItemAPIMock {
fileMock := &clientmock.FileAPIMock{}
fileMock.On("Read", mock.Anything, "vault-id", "item-id",
mock.MatchedBy(func(attr sdk.FileAttributes) bool {
return attr.ID == "file-id"
}),
).Return([]byte("file content"), nil)
itemMock := &clientmock.ItemAPIMock{
FilesAPI: fileMock,
}
itemMock.On("Files").Return(fileMock)
return itemMock
},
check: func(t *testing.T, content []byte, err error) {
require.NoError(t, err)
require.Equal(t, []byte("file content"), content)
},
},
"should return an error": {
mockItemAPI: func() *clientmock.ItemAPIMock {
fileMock := &clientmock.FileAPIMock{}
fileMock.On("Read", mock.Anything, "vault-id", "item-id",
mock.MatchedBy(func(attr sdk.FileAttributes) bool {
return attr.ID == "file-id"
}),
).Return(nil, errors.New("error"))
itemMock := &clientmock.ItemAPIMock{
FilesAPI: fileMock,
}
itemMock.On("Files").Return(fileMock)
return itemMock
},
check: func(t *testing.T, content []byte, err error) {
require.Error(t, err)
require.Nil(t, content)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &SDK{
client: &sdk.Client{
ItemsAPI: tc.mockItemAPI(),
},
}
content, err := client.GetFileContent("vault-id", "item-id", "file-id")
tc.check(t, content, err)
})
}
}
func TestSDK_GetVaultsByTitle(t *testing.T) {
now := time.Now()
testCases := map[string]struct {
mockVaultAPI func() *clientmock.VaultAPIMock
check func(t *testing.T, vaults []model.Vault, err error)
}{
"should return a single vault": {
mockVaultAPI: func() *clientmock.VaultAPIMock {
m := &clientmock.VaultAPIMock{}
m.On("List", context.Background()).Return([]sdk.VaultOverview{
{
ID: "test-id",
Title: VaultTitleEmployee,
CreatedAt: now,
},
{
ID: "test-id-2",
Title: "Some other vault",
CreatedAt: now,
},
}, nil)
return m
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.NoError(t, err)
require.Len(t, vaults, 1)
require.Equal(t, "test-id", vaults[0].ID)
require.Equal(t, now, vaults[0].CreatedAt)
},
},
"should return a two vaults": {
mockVaultAPI: func() *clientmock.VaultAPIMock {
m := &clientmock.VaultAPIMock{}
m.On("List", context.Background()).Return([]sdk.VaultOverview{
{
ID: "test-id",
Title: VaultTitleEmployee,
CreatedAt: now,
},
{
ID: "test-id-2",
Title: VaultTitleEmployee,
CreatedAt: now,
},
}, nil)
return m
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.NoError(t, err)
require.Len(t, vaults, 2)
// Check the first vault
require.Equal(t, "test-id", vaults[0].ID)
require.Equal(t, now, vaults[0].CreatedAt)
// Check the second vault
require.Equal(t, "test-id-2", vaults[1].ID)
require.Equal(t, now, vaults[1].CreatedAt)
},
},
"should return an error": {
mockVaultAPI: func() *clientmock.VaultAPIMock {
m := &clientmock.VaultAPIMock{}
m.On("List", context.Background()).Return([]sdk.VaultOverview{}, errors.New("error"))
return m
},
check: func(t *testing.T, vaults []model.Vault, err error) {
require.Error(t, err)
require.Empty(t, vaults)
},
},
}
for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
client := &SDK{
client: &sdk.Client{
VaultsAPI: tc.mockVaultAPI(),
},
}
vault, err := client.GetVaultsByTitle(VaultTitleEmployee)
tc.check(t, vault, err)
})
}
}

View File

@@ -0,0 +1,110 @@
package testing
import (
sdk "github.com/1password/onepassword-sdk-go"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
func CreateConnectItem() *onepassword.Item {
return &onepassword.Item{
ID: "test-id",
Vault: onepassword.ItemVault{ID: "test-vault-id"},
Version: 1,
Tags: []string{"tag1", "tag2"},
Fields: []*onepassword.ItemField{
{Label: "label1", Value: "value1"},
{Label: "label2", Value: "value2"},
},
Files: []*onepassword.File{
{ID: "file-id-1", Name: "file1.txt", Size: 1234},
{ID: "file-id-2", Name: "file2.txt", Size: 1234},
},
}
}
func CreateSDKItem() *sdk.Item {
return &sdk.Item{
ID: "test-id",
VaultID: "test-vault-id",
Version: 1,
Tags: []string{"tag1", "tag2"},
Fields: []sdk.ItemField{
{Title: "label1", Value: "value1"},
{Title: "label2", Value: "value2"},
},
Files: []sdk.ItemFile{
{Attributes: sdk.FileAttributes{ID: "file-id-1", Name: "file1.txt", Size: 1234}},
{Attributes: sdk.FileAttributes{ID: "file-id-2", Name: "file2.txt", Size: 1234}},
},
CreatedAt: time.Now(),
}
}
func CreateSDKItemOverview() *sdk.ItemOverview {
return &sdk.ItemOverview{
ID: "test-id",
Title: "item-title",
VaultID: "test-vault-id",
Tags: []string{"tag1", "tag2"},
CreatedAt: time.Now(),
}
}
func CheckConnectItemMapping(t *testing.T, expected *onepassword.Item, actual *model.Item) {
t.Helper()
require.Equal(t, expected.ID, actual.ID)
require.Equal(t, expected.Vault.ID, actual.VaultID)
require.Equal(t, expected.Version, actual.Version)
require.ElementsMatch(t, expected.Tags, actual.Tags)
for i, field := range expected.Fields {
require.Equal(t, field.Label, actual.Fields[i].Label)
require.Equal(t, field.Value, actual.Fields[i].Value)
}
for i, file := range expected.Files {
require.Equal(t, file.ID, actual.Files[i].ID)
require.Equal(t, file.Name, actual.Files[i].Name)
require.Equal(t, file.Size, actual.Files[i].Size)
}
require.Equal(t, expected.CreatedAt, actual.CreatedAt)
}
func CheckSDKItemMapping(t *testing.T, expected *sdk.Item, actual *model.Item) {
t.Helper()
require.Equal(t, expected.ID, actual.ID)
require.Equal(t, expected.VaultID, actual.VaultID)
require.Equal(t, int(expected.Version), actual.Version)
require.ElementsMatch(t, expected.Tags, actual.Tags)
for i, field := range expected.Fields {
require.Equal(t, field.Title, actual.Fields[i].Label)
require.Equal(t, field.Value, actual.Fields[i].Value)
}
for i, file := range expected.Files {
require.Equal(t, file.Attributes.ID, actual.Files[i].ID)
require.Equal(t, file.Attributes.Name, actual.Files[i].Name)
require.Equal(t, int(file.Attributes.Size), actual.Files[i].Size)
}
require.Equal(t, expected.CreatedAt, actual.CreatedAt)
}
func CheckSDKItemOverviewMapping(t *testing.T, expected *sdk.ItemOverview, actual *model.Item) {
t.Helper()
require.Equal(t, expected.ID, actual.ID)
require.Equal(t, expected.VaultID, actual.VaultID)
require.ElementsMatch(t, expected.Tags, actual.Tags)
require.Equal(t, expected.CreatedAt, actual.CreatedAt)
}

View File

@@ -0,0 +1,130 @@
package mock
import (
"github.com/stretchr/testify/mock"
"github.com/1Password/connect-sdk-go/onepassword"
)
// ConnectClientMock is a mock implementation of the ConnectClient interface
type ConnectClientMock struct {
mock.Mock
}
func (c *ConnectClientMock) GetVaults() ([]onepassword.Vault, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetVault(uuid string) (*onepassword.Vault, error) {
args := c.Called(uuid)
return args.Get(0).(*onepassword.Vault), args.Error(1)
}
func (c *ConnectClientMock) GetVaultByUUID(uuid string) (*onepassword.Vault, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetVaultByTitle(title string) (*onepassword.Vault, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
args := c.Called(title)
return args.Get(0).([]onepassword.Vault), args.Error(1)
}
func (c *ConnectClientMock) GetItems(vaultQuery string) ([]onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetItemByUUID(uuid string, vaultQuery string) (*onepassword.Item, error) {
args := c.Called(uuid, vaultQuery)
return args.Get(0).(*onepassword.Item), args.Error(1)
}
func (c *ConnectClientMock) GetItemByTitle(title string, vaultQuery string) (*onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetItemsByTitle(title string, vaultQuery string) ([]onepassword.Item, error) {
args := c.Called(title, vaultQuery)
return args.Get(0).([]onepassword.Item), args.Error(1)
}
func (c *ConnectClientMock) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) DeleteItem(item *onepassword.Item, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) DeleteItemByID(itemUUID string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) DeleteItemByTitle(title string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetFiles(itemQuery string, vaultQuery string) ([]onepassword.File, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetFile(uuid string, itemQuery string, vaultQuery string) (*onepassword.File, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) GetFileContent(file *onepassword.File) ([]byte, error) {
args := c.Called(file)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}
func (c *ConnectClientMock) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) LoadStructFromItemByUUID(config interface{}, itemUUID string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) LoadStructFromItemByTitle(config interface{}, itemTitle string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) LoadStructFromItem(config interface{}, itemQuery string, vaultQuery string) error {
// Only implement this if mocking is needed
panic("implement me")
}
func (c *ConnectClientMock) LoadStruct(config interface{}) error {
// Only implement this if mocking is needed
panic("implement me")
}

View File

@@ -0,0 +1,89 @@
package mock
import (
"context"
"github.com/stretchr/testify/mock"
sdk "github.com/1password/onepassword-sdk-go"
)
type VaultAPIMock struct {
mock.Mock
}
func (v *VaultAPIMock) List(ctx context.Context) ([]sdk.VaultOverview, error) {
args := v.Called(ctx)
return args.Get(0).([]sdk.VaultOverview), args.Error(1)
}
type ItemAPIMock struct {
mock.Mock
FilesAPI sdk.ItemsFilesAPI
}
func (i *ItemAPIMock) Create(ctx context.Context, params sdk.ItemCreateParams) (sdk.Item, error) {
//TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) Get(ctx context.Context, vaultID string, itemID string) (sdk.Item, error) {
args := i.Called(ctx, vaultID, itemID)
return args.Get(0).(sdk.Item), args.Error(1)
}
func (i *ItemAPIMock) Put(ctx context.Context, item sdk.Item) (sdk.Item, error) {
//TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) Delete(ctx context.Context, vaultID string, itemID string) error {
//TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) Archive(ctx context.Context, vaultID string, itemID string) error {
//TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) List(ctx context.Context, vaultID string, filters ...sdk.ItemListFilter) ([]sdk.ItemOverview, error) {
args := i.Called(ctx, vaultID, filters)
return args.Get(0).([]sdk.ItemOverview), args.Error(1)
}
func (i *ItemAPIMock) Shares() sdk.ItemsSharesAPI {
//TODO implement me
panic("implement me")
}
func (i *ItemAPIMock) Files() sdk.ItemsFilesAPI {
return i.FilesAPI
}
type FileAPIMock struct {
mock.Mock
}
func (f *FileAPIMock) Attach(ctx context.Context, item sdk.Item, fileParams sdk.FileCreateParams) (sdk.Item, error) {
//TODO implement me
panic("implement me")
}
func (f *FileAPIMock) Delete(ctx context.Context, item sdk.Item, sectionID string, fieldID string) (sdk.Item, error) {
//TODO implement me
panic("implement me")
}
func (f *FileAPIMock) ReplaceDocument(ctx context.Context, item sdk.Item, docParams sdk.DocumentCreateParams) (sdk.Item, error) {
//TODO implement me
panic("implement me")
}
func (f *FileAPIMock) Read(ctx context.Context, vaultID, itemID string, attributes sdk.FileAttributes) ([]byte, error) {
args := f.Called(ctx, vaultID, itemID, attributes)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}

View File

@@ -4,36 +4,36 @@ import (
"fmt"
"strings"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
logf "sigs.k8s.io/controller-runtime/pkg/log"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
)
var logger = logf.Log.WithName("retrieve_item")
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
func GetOnePasswordItemByPath(opClient opclient.Client, path string) (*model.Item, error) {
vaultNameOrID, itemNameOrID, err := ParseVaultAndItemFromPath(path)
if err != nil {
return nil, err
}
vaultId, err := getVaultId(opConnectClient, vaultValue)
vaultID, err := getVaultID(opClient, vaultNameOrID)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to 'getVaultID' for vaultNameOrID='%s': %w", vaultNameOrID, err)
}
itemId, err := getItemId(opConnectClient, itemValue, vaultId)
itemID, err := getItemID(opClient, vaultID, itemNameOrID)
if err != nil {
return nil, err
return nil, fmt.Errorf("faild to 'getItemID' for vaultID='%s' and itemNameOrID='%s': %w", vaultID, itemNameOrID, err)
}
item, err := opConnectClient.GetItem(itemId, vaultId)
item, err := opClient.GetItemByID(vaultID, itemID)
if err != nil {
return nil, err
return nil, fmt.Errorf("faield to 'GetItemByID' for vaultID='%s' and itemID='%s': %w", vaultID, itemID, err)
}
for _, file := range item.Files {
_, err := opConnectClient.GetFileContent(file)
_, err := opClient.GetFileContent(vaultID, itemID, file.ID)
if err != nil {
return nil, err
}
@@ -50,15 +50,15 @@ func ParseVaultAndItemFromPath(path string) (string, string, error) {
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
}
func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {
if !IsValidClientUUID(vaultIdentifier) {
vaults, err := client.GetVaultsByTitle(vaultIdentifier)
func getVaultID(client opclient.Client, vaultNameOrID string) (string, error) {
if !IsValidClientUUID(vaultNameOrID) {
vaults, err := client.GetVaultsByTitle(vaultNameOrID)
if err != nil {
return "", err
}
if len(vaults) == 0 {
return "", fmt.Errorf("No vaults found with identifier %q", vaultIdentifier)
return "", fmt.Errorf("No vaults found with identifier %q", vaultNameOrID)
}
oldestVault := vaults[0]
@@ -68,22 +68,22 @@ func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {
oldestVault = returnedVault
}
}
logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultIdentifier, oldestVault.ID))
logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultNameOrID, oldestVault.ID))
}
vaultIdentifier = oldestVault.ID
vaultNameOrID = oldestVault.ID
}
return vaultIdentifier, nil
return vaultNameOrID, nil
}
func getItemId(client connect.Client, itemIdentifier string, vaultId string) (string, error) {
if !IsValidClientUUID(itemIdentifier) {
items, err := client.GetItemsByTitle(itemIdentifier, vaultId)
func getItemID(client opclient.Client, vaultId, itemNameOrID string) (string, error) {
if !IsValidClientUUID(itemNameOrID) {
items, err := client.GetItemsByTitle(vaultId, itemNameOrID)
if err != nil {
return "", err
}
if len(items) == 0 {
return "", fmt.Errorf("No items found with identifier %q", itemIdentifier)
return "", fmt.Errorf("No items found with identifier %q", itemNameOrID)
}
oldestItem := items[0]
@@ -93,9 +93,9 @@ func getItemId(client connect.Client, itemIdentifier string, vaultId string) (st
oldestItem = returnedItem
}
}
logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemIdentifier, oldestItem.ID))
logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemNameOrID, oldestItem.ID))
}
itemIdentifier = oldestItem.ID
itemNameOrID = oldestItem.ID
}
return itemIdentifier, nil
return itemNameOrID, nil
}

View File

@@ -0,0 +1,27 @@
package model
import (
"errors"
)
// File represents a file stored in 1Password.
type File struct {
ID string
Name string
Size int
ContentPath string
content []byte
}
// Content returns the content of the file if they have been loaded and returns an error if they have not been loaded.
// Use `client.GetFileContent(file *File)` instead to make sure the content is fetched automatically if not present.
func (f *File) Content() ([]byte, error) {
if f.content == nil {
return nil, errors.New("file content not loaded")
}
return f.content, nil
}
func (f *File) SetContent(content []byte) {
f.content = content
}

View File

@@ -0,0 +1,85 @@
package model
import (
"time"
connect "github.com/1Password/connect-sdk-go/onepassword"
sdk "github.com/1password/onepassword-sdk-go"
)
// Item represents 1Password item.
type Item struct {
ID string
VaultID string
Version int
Tags []string
Fields []ItemField
Files []File
CreatedAt time.Time
}
// FromConnectItem populates the Item from a Connect item.
func (i *Item) FromConnectItem(item *connect.Item) {
i.ID = item.ID
i.VaultID = item.Vault.ID
i.Version = item.Version
for _, tag := range item.Tags {
i.Tags = append(i.Tags, tag)
}
for _, field := range item.Fields {
i.Fields = append(i.Fields, ItemField{
Label: field.Label,
Value: field.Value,
})
}
for _, file := range item.Files {
i.Files = append(i.Files, File{
ID: file.ID,
Name: file.Name,
Size: file.Size,
})
}
i.CreatedAt = item.CreatedAt
}
// FromSDKItem populates the Item from an SDK item.
func (i *Item) FromSDKItem(item *sdk.Item) {
i.ID = item.ID
i.VaultID = item.VaultID
i.Version = int(item.Version)
i.Tags = make([]string, len(item.Tags))
copy(i.Tags, item.Tags)
for _, field := range item.Fields {
i.Fields = append(i.Fields, ItemField{
Label: field.Title,
Value: field.Value,
})
}
for _, file := range item.Files {
i.Files = append(i.Files, File{
ID: file.Attributes.ID,
Name: file.Attributes.Name,
Size: int(file.Attributes.Size),
})
}
i.CreatedAt = item.CreatedAt
}
// FromSDKItemOverview populates the Item from an SDK item overview.
func (i *Item) FromSDKItemOverview(item *sdk.ItemOverview) {
i.ID = item.ID
i.VaultID = item.VaultID
i.Tags = make([]string, len(item.Tags))
copy(i.Tags, item.Tags)
i.CreatedAt = item.CreatedAt
}

View File

@@ -0,0 +1,7 @@
package model
// ItemField Representation of a single field on an Item
type ItemField struct {
Label string
Value string
}

View File

@@ -0,0 +1,108 @@
package model
import (
"testing"
"time"
"github.com/stretchr/testify/require"
connect "github.com/1Password/connect-sdk-go/onepassword"
sdk "github.com/1password/onepassword-sdk-go"
)
func TestItem_FromConnectItem(t *testing.T) {
connectItem := &connect.Item{
ID: "test-item-id",
Vault: connect.ItemVault{
ID: "test-vault-id",
},
Version: 1,
Tags: []string{"tag1", "tag2"},
Fields: []*connect.ItemField{
{Label: "field1", Value: "value1"},
{Label: "field2", Value: "value2"},
},
Files: []*connect.File{
{ID: "file1", Name: "file1.txt", Size: 1234},
{ID: "file2", Name: "file2.txt", Size: 1234},
},
CreatedAt: time.Now(),
}
item := &Item{}
item.FromConnectItem(connectItem)
require.Equal(t, connectItem.ID, item.ID)
require.Equal(t, connectItem.Vault.ID, item.VaultID)
require.Equal(t, connectItem.Version, item.Version)
require.ElementsMatch(t, connectItem.Tags, item.Tags)
for i, field := range connectItem.Fields {
require.Equal(t, field.Label, item.Fields[i].Label)
require.Equal(t, field.Value, item.Fields[i].Value)
}
for i, file := range connectItem.Files {
require.Equal(t, file.ID, item.Files[i].ID)
require.Equal(t, file.Name, item.Files[i].Name)
require.Equal(t, file.Size, item.Files[i].Size)
}
require.Equal(t, connectItem.CreatedAt, item.CreatedAt)
}
func TestItem_FromSDKItem(t *testing.T) {
sdkItem := &sdk.Item{
ID: "test-item-id",
VaultID: "test-vault-id",
Version: 1,
Tags: []string{"tag1", "tag2"},
Fields: []sdk.ItemField{
{ID: "1", Title: "field1", Value: "value1"},
{ID: "2", Title: "field2", Value: "value2"},
},
Files: []sdk.ItemFile{
{Attributes: sdk.FileAttributes{Name: "file1.txt", Size: 1234}, FieldID: "file1"},
{Attributes: sdk.FileAttributes{Name: "file2.txt", Size: 1234}, FieldID: "file2"},
},
CreatedAt: time.Now(),
}
item := &Item{}
item.FromSDKItem(sdkItem)
require.Equal(t, sdkItem.ID, item.ID)
require.Equal(t, sdkItem.VaultID, item.VaultID)
require.Equal(t, int(sdkItem.Version), item.Version)
require.ElementsMatch(t, sdkItem.Tags, item.Tags)
for i, field := range sdkItem.Fields {
require.Equal(t, field.Title, item.Fields[i].Label)
require.Equal(t, field.Value, item.Fields[i].Value)
}
for i, file := range sdkItem.Files {
require.Equal(t, file.Attributes.ID, item.Files[i].ID)
require.Equal(t, file.Attributes.Name, item.Files[i].Name)
require.Equal(t, int(file.Attributes.Size), item.Files[i].Size)
}
require.Equal(t, sdkItem.CreatedAt, item.CreatedAt)
}
func TestItem_FromSDKItemOverview(t *testing.T) {
sdkItemOverview := &sdk.ItemOverview{
ID: "test-item-id",
VaultID: "test-vault-id",
Tags: []string{"tag1", "tag2"},
CreatedAt: time.Now(),
}
item := &Item{}
item.FromSDKItemOverview(sdkItemOverview)
require.Equal(t, sdkItemOverview.ID, item.ID)
require.Equal(t, sdkItemOverview.VaultID, item.VaultID)
require.ElementsMatch(t, sdkItemOverview.Tags, item.Tags)
require.Equal(t, sdkItemOverview.CreatedAt, item.CreatedAt)
}

View File

@@ -0,0 +1,23 @@
package model
import (
"time"
connect "github.com/1Password/connect-sdk-go/onepassword"
sdk "github.com/1password/onepassword-sdk-go"
)
type Vault struct {
ID string
CreatedAt time.Time
}
func (v *Vault) FromConnectVault(vault *connect.Vault) {
v.ID = vault.ID
v.CreatedAt = vault.CreatedAt
}
func (v *Vault) FromSDKVault(vault *sdk.VaultOverview) {
v.ID = vault.ID
v.CreatedAt = vault.CreatedAt
}

View File

@@ -0,0 +1,37 @@
package model
import (
"testing"
"time"
"github.com/stretchr/testify/require"
connect "github.com/1Password/connect-sdk-go/onepassword"
sdk "github.com/1password/onepassword-sdk-go"
)
func TestVault_FromConnectVault(t *testing.T) {
connectVault := &connect.Vault{
ID: "test-id",
CreatedAt: time.Now(),
}
vault := &Vault{}
vault.FromConnectVault(connectVault)
require.Equal(t, connectVault.ID, vault.ID)
require.Equal(t, connectVault.CreatedAt, vault.CreatedAt)
}
func TestVault_FromSDKVault(t *testing.T) {
sdkVault := &sdk.VaultOverview{
ID: "test-id",
CreatedAt: time.Now(),
}
vault := &Vault{}
vault.FromSDKVault(sdkVault)
require.Equal(t, sdkVault.ID, vault.ID)
require.Equal(t, sdkVault.CreatedAt, vault.CreatedAt)
}

View File

@@ -8,10 +8,10 @@ import (
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
"github.com/1Password/onepassword-operator/pkg/utils"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -23,17 +23,17 @@ const lockTag = "operator.1password.io:ignore-secret"
var log = logf.Log.WithName("update_op_kubernetes_secrets_task")
func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler {
func NewManager(kubernetesClient client.Client, opClient opclient.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler {
return &SecretUpdateHandler{
client: kubernetesClient,
opConnectClient: opConnectClient,
opClient: opClient,
shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal,
}
}
type SecretUpdateHandler struct {
client client.Client
opConnectClient connect.Client
opClient opclient.Client
shouldAutoRestartDeploymentsGlobal bool
}
@@ -121,14 +121,14 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
OnePasswordItemPath := h.getPathFromOnePasswordItem(secret)
item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath)
item, err := GetOnePasswordItemByPath(h.opClient, OnePasswordItemPath)
if err != nil {
log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name)
continue
}
itemVersion := fmt.Sprint(item.Version)
itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.VaultID, item.ID)
if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString {
if isItemLockedForForcedRestarts(item) {
@@ -159,7 +159,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
return updatedSecrets, nil
}
func isItemLockedForForcedRestarts(item *onepassword.Item) bool {
func isItemLockedForForcedRestarts(item *model.Item) bool {
tags := item.Tags
for i := 0; i < len(tags); i++ {
if tags[i] == lockTag {

View File

@@ -4,11 +4,14 @@ import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/1Password/onepassword-operator/pkg/mocks"
"github.com/1Password/onepassword-operator/pkg/onepassword/model"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
errors2 "k8s.io/apimachinery/pkg/api/errors"
@@ -802,19 +805,11 @@ func TestUpdateSecretHandler(t *testing.T) {
// Create a fake client to mock API calls.
cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
opConnectClient := &mocks.TestClient{}
mocks.DoGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{}
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
item.Version = itemVersion
item.Vault.ID = vaultUUID
item.ID = uuid
return &item, nil
}
mockOpClient := &mocks.TestClient{}
mockOpClient.On("GetItemByID", mock.Anything, mock.Anything).Return(createItem(), nil)
h := &SecretUpdateHandler{
client: cl,
opConnectClient: opConnectClient,
opClient: mockOpClient,
shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled,
}
@@ -879,16 +874,23 @@ func TestIsUpdatedSecret(t *testing.T) {
assert.True(t, isUpdatedSecret(secretName, updatedSecrets))
}
func generateFields(username, password string) []*onepassword.ItemField {
fields := []*onepassword.ItemField{
{
Label: "username",
Value: username,
},
{
Label: "password",
Value: password,
func createItem() *model.Item {
return &model.Item{
ID: itemId,
VaultID: vaultId,
Version: itemVersion,
Tags: []string{"tag1", "tag2"},
Fields: []model.ItemField{
{
Label: "username",
Value: username,
},
{
Label: "password",
Value: password,
},
},
Files: []model.File{},
CreatedAt: time.Now(),
}
return fields
}