Compare commits

..

1 Commits

Author SHA1 Message Date
jillianwilson
1590dd9b89 Updating path for fetching 1password items to be of the op:// reference format 2021-09-13 10:33:34 -03:00
38 changed files with 299 additions and 1534 deletions

View File

@@ -1 +1 @@
1.3.0
1.0.2

View File

@@ -12,31 +12,6 @@
---
[//]: # (START/v1.3.0)
# v1.3.0
## Features
* Added support for loading secrets from files stored in 1Password. {#47}
---
[//]: # (START/v1.2.0)
# v1.2.0
## Features
* Support secrets provisioned through FromEnv. {#74}
* Support configuration of Kubernetes Secret type. {#87}
* Improved logging. (#72)
---
[//]: # (START/v1.1.0)
# v1.1.0
## Fixes
* Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
---
[//]: # (START/v1.0.2)
# v1.0.2

View File

@@ -2,7 +2,7 @@
The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item.
The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment.
The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Reference on a deployment.
The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted.
@@ -30,13 +30,14 @@ If 1Password Connect is already running, you can skip this step. This guide will
Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session:
```bash
cat 1password-credentials.json | base64 | \
$ cat 1password-credentials.json | base64 | \
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
```
Create a Kubernetes secret from the op-session file:
```bash
kubectl create secret generic op-credentials --from-file=op-session
$ kubectl create secret generic op-credentials --from-file=1password-credentials.json
```
Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`:
@@ -52,12 +53,12 @@ Adding this environment variable will have the operator automatically deploy a d
"Create a Connect token for the operator and save it as a Kubernetes Secret:
```bash
kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>"
$ kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>"
```
If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command:
```bash
kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
$ kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
```
[More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens)
@@ -67,13 +68,13 @@ kubectl create secret generic onepassword-token --from-literal=token=$(op create
We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder.
```bash
kubectl apply -f deploy/permissions.yaml
$ kubectl apply -f deploy/permissions.yaml
```
**Create Custom One Password Secret Resource**
```bash
kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
$ kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
```
**Deploying the Operator**
@@ -105,19 +106,19 @@ 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>"
itemReference: "op://<vault_id_or_title>/<item_id_or_title>"
```
Deploy the OnePasswordItem to Kubernetes:
```bash
kubectl apply -f <your_item>.yaml
$ kubectl apply -f <your_item>.yaml
```
To test that the Kubernetes Secret check that the following command returns a secret:
```bash
kubectl get secret <secret_name>
$ kubectl get secret <secret_name>
```
Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.
@@ -130,25 +131,20 @@ 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-reference: "op://<vault>/<item>"
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.
Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Reference.
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.
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-reference` and `operator.1password.io/item-name` and no other deployment is using the secret.
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
---
**NOTE**
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.
If multiple 1Password vaults/items have the same `title` when using a title in the access reference, the desired action will be performed on the oldest vault/item.
Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format:
- Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed
@@ -175,7 +171,7 @@ metadata:
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment.
If the value is not set, the auto reset settings on the operator will be used. This value can be overwritten by deployment.
**Per Deployment**
This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below:
@@ -188,7 +184,7 @@ metadata:
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the namespace will be used.
If the value is not set, the auto reset settings on the namespace will be used.
**Per OnePasswordItem Custom Resource**
This method allows for managing auto restarts on a given OnePasswordItem custom resource. Auto restarts can by managed by setting the annotation `operator.1password.io/auto_restart` to either `true` or `false` on the desired OnePasswordItem. An example of this is shown below:
@@ -201,7 +197,7 @@ metadata:
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the deployment will be used.
If the value is not set, the auto reset settings on the deployment will be used.
## Development

View File

@@ -179,9 +179,7 @@ func main() {
return
case <-ticker.C:
err := updatedSecretsPoller.UpdateKubernetesSecretsTask()
if err != nil {
log.Error(err, "error running update kubernetes secret task")
}
log.Error(err, "Error occured during update secret task")
}
}
}()

View File

@@ -33,13 +33,10 @@ spec:
spec:
description: OnePasswordItemSpec defines the desired state of OnePasswordItem
properties:
itemPath:
itemReference:
type: string
type: object
status:
description: OnePasswordItemStatus defines the observed state of OnePasswordItem
type: object
type:
description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
type: string
type: object

View File

@@ -3,4 +3,4 @@ kind: OnePasswordItem
metadata:
name: example
spec:
itemPath: "vaults/<vault_id>/items/<item_id>"
itemReference: "op://<vault_id>/<item_id>"

View File

@@ -16,6 +16,7 @@ spec:
containers:
- name: onepassword-connect-operator
image: 1password/onepassword-operator
imagePullPolicy: Never
command: ["/manager"]
env:
- name: WATCH_NAMESPACE

4
go.mod
View File

@@ -3,11 +3,11 @@ module github.com/1Password/onepassword-operator
go 1.13
require (
github.com/1Password/connect-sdk-go v1.2.0
github.com/1Password/connect-sdk-go v1.0.1
github.com/operator-framework/operator-sdk v0.19.0
github.com/prometheus/common v0.14.0 // indirect
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.6.1
k8s.io/api v0.18.2
k8s.io/apimachinery v0.18.2
k8s.io/client-go v12.0.0+incompatible

4
go.sum
View File

@@ -20,8 +20,6 @@ contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeS
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/1Password/connect-sdk-go v1.0.1 h1:BOeMIxVk6/ISmLNWUkSxEbVI7tNr5+aNXIobMM0/I0U=
github.com/1Password/connect-sdk-go v1.0.1/go.mod h1:br2BWk2sqgJFnOFK5WSDfBBmwQ6E7hV9LoPqrtHGRNY=
github.com/1Password/connect-sdk-go v1.2.0 h1:WbIvmbDUpA89nyH0l3LF2iRSFJAv86d2D7IjVNjw6iw=
github.com/1Password/connect-sdk-go v1.2.0/go.mod h1:qK2bF/GweAq812xj+HGfbauaE6cKX1MXfKhpAvoHEq8=
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
@@ -887,8 +885,6 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/thanos-io/thanos v0.11.0/go.mod h1:N/Yes7J68KqvmY+xM6J5CJqEvWIvKSR5sqGtmuD6wDc=
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=

View File

@@ -8,7 +8,7 @@ import (
// OnePasswordItemSpec defines the desired state of OnePasswordItem
type OnePasswordItemSpec struct {
ItemPath string `json:"itemPath,omitempty"`
ItemReference string `json:"itemReference,omitempty"`
}
// OnePasswordItemStatus defines the observed state of OnePasswordItem
@@ -26,7 +26,6 @@ type OnePasswordItemStatus struct {
type OnePasswordItem struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Type string `json:"type,omitempty"`
Spec OnePasswordItemSpec `json:"spec,omitempty"`
Status OnePasswordItemStatus `json:"status,omitempty"`

View File

@@ -191,18 +191,15 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat
reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
secretName := annotations[op.NameAnnotation]
secretLabels := map[string]string(nil)
secretType := ""
if len(secretName) == 0 {
reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.")
reqLog.Info("No 'item-name' annotation set. 'item-reference' and 'item-name' must be set as annotations to add new secret.")
return nil
}
item, err := op.GetOnePasswordItemByPath(r.opConnectClient, annotations[op.ItemPathAnnotation])
item, err := op.GetOnePasswordItemByReference(r.opConnectClient, annotations[op.ItemReferenceAnnotation])
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, annotations)
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation])
}

View File

@@ -52,7 +52,7 @@ var (
"password": []byte(password),
"username": []byte(username),
}
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
ItemReference = fmt.Sprintf("op://%v/%v", vaultId, itemId)
)
var (
@@ -76,8 +76,8 @@ var tests = []testReconcileItem{
finalizer,
},
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.ItemReferenceAnnotation: ItemReference,
op.NameAnnotation: name,
},
},
},
@@ -90,8 +90,8 @@ var tests = []testReconcileItem{
Name: "another-deployment",
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.ItemReferenceAnnotation: ItemReference,
op.NameAnnotation: name,
},
},
Spec: appsv1.DeploymentSpec{
@@ -152,8 +152,8 @@ var tests = []testReconcileItem{
finalizer,
},
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.ItemReferenceAnnotation: ItemReference,
op.NameAnnotation: name,
},
},
},
@@ -166,8 +166,8 @@ var tests = []testReconcileItem{
Name: "another-deployment",
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.ItemReferenceAnnotation: ItemReference,
op.NameAnnotation: name,
},
},
Spec: appsv1.DeploymentSpec{
@@ -235,8 +235,8 @@ var tests = []testReconcileItem{
finalizer,
},
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.ItemReferenceAnnotation: ItemReference,
op.NameAnnotation: name,
},
},
},
@@ -258,7 +258,7 @@ var tests = []testReconcileItem{
},
},
{
testName: "Test Do not update if Annotations have not changed",
testName: "Test Do not update if OnePassword Item Version has not changed",
deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
@@ -268,10 +268,9 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.ItemReferenceAnnotation: ItemReference,
op.NameAnnotation: name,
},
Labels: map[string]string{},
},
},
existingSecret: &corev1.Secret{
@@ -279,9 +278,7 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
@@ -292,11 +289,8 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.VersionAnnotation: fmt.Sprint(version),
},
Labels: map[string]string(nil),
},
Data: expectedSecretData,
},
@@ -316,8 +310,8 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.ItemReferenceAnnotation: ItemReference,
op.NameAnnotation: name,
},
},
},
@@ -329,7 +323,6 @@ var tests = []testReconcileItem{
op.VersionAnnotation: "456",
},
},
Type: corev1.SecretType(""),
Data: expectedSecretData,
},
expectedError: nil,
@@ -341,7 +334,6 @@ var tests = []testReconcileItem{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretType(""),
Data: expectedSecretData,
},
opItem: map[string]string{
@@ -360,8 +352,8 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
op.ItemReferenceAnnotation: ItemReference,
op.NameAnnotation: name,
},
},
},
@@ -375,7 +367,6 @@ var tests = []testReconcileItem{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretType(""),
Data: expectedSecretData,
},
opItem: map[string]string{

View File

@@ -144,15 +144,12 @@ func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem
func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
secretName := resource.GetName()
labels := resource.Labels
annotations := resource.Annotations
secretType := resource.Type
autoRestart := annotations[op.RestartDeploymentsAnnotation]
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath)
item, err := onepassword.GetOnePasswordItemByReference(r.opConnectClient, resource.Spec.ItemReference)
if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err)
}
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, secretType, annotations)
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart)
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"testing"
"github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
@@ -56,7 +55,7 @@ var (
"password": []byte(password),
"username": []byte(username),
}
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
itemReference = fmt.Sprintf("op://%v/%v", vaultId, itemId)
)
var (
@@ -80,7 +79,7 @@ var tests = []testReconcileItem{
},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
ItemReference: itemReference,
},
},
existingSecret: &corev1.Secret{
@@ -112,7 +111,7 @@ var tests = []testReconcileItem{
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
ItemReference: itemReference,
},
},
existingSecret: &corev1.Secret{
@@ -120,8 +119,7 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
@@ -132,8 +130,7 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: expectedSecretData,
@@ -153,14 +150,9 @@ var tests = []testReconcileItem{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
ItemReference: itemReference,
},
},
existingSecret: &corev1.Secret{
@@ -168,10 +160,8 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: "456",
op.ItemPathAnnotation: itemPath,
op.VersionAnnotation: "456",
},
Labels: map[string]string{},
},
Data: expectedSecretData,
},
@@ -181,10 +171,8 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.VersionAnnotation: fmt.Sprint(version),
},
Labels: map[string]string{},
},
Data: expectedSecretData,
},
@@ -193,59 +181,6 @@ var tests = []testReconcileItem{
passKey: password,
},
},
{
testName: "Test Updating Type of Existing Kubernetes Secret using OnePasswordItem",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
Type: string(corev1.SecretTypeBasicAuth),
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Type: corev1.SecretTypeBasicAuth,
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Type: corev1.SecretTypeBasicAuth,
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Custom secret type",
customResource: &onepasswordv1.OnePasswordItem{
@@ -258,9 +193,8 @@ var tests = []testReconcileItem{
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
ItemReference: itemReference,
},
Type: "custom",
},
existingSecret: nil,
expectedError: nil,
@@ -272,51 +206,6 @@ var tests = []testReconcileItem{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretType("custom"),
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Error if secret type is changed",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
Type: "custom",
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretTypeOpaque,
Data: expectedSecretData,
},
expectedError: kubernetessecrets.ErrCannotUpdateSecretType,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretTypeOpaque,
Data: expectedSecretData,
},
opItem: map[string]string{
@@ -336,7 +225,7 @@ var tests = []testReconcileItem{
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
ItemReference: itemReference,
},
},
existingSecret: nil,
@@ -368,7 +257,7 @@ var tests = []testReconcileItem{
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
ItemReference: itemReference,
},
},
existingSecret: nil,
@@ -385,7 +274,7 @@ var tests = []testReconcileItem{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"aws-access-key": []byte(awsKey),
"ice-cream-type": []byte(iceCream),
},
},
@@ -397,47 +286,6 @@ var tests = []testReconcileItem{
"😄 ice-cream type": iceCream,
},
},
{
testName: "Secret from 1Password item with `-`, `_` and `.`",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!.my_sECReT.it3m%-_",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret.it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: map[string][]byte{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"-_ice_cream.type.": []byte(iceCream),
},
},
opItem: map[string]string{
userKey: username,
passKey: password,
"first host": firstHost,
"AWS Access Key": awsKey,
"😄 -_ice_cream.type.": iceCream,
},
},
}
func TestReconcileOnePasswordItem(t *testing.T) {

View File

@@ -7,12 +7,7 @@ import (
"regexp"
"strings"
"reflect"
errs "errors"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@@ -28,36 +23,27 @@ const OnepasswordPrefix = "operator.1password.io"
const NameAnnotation = OnepasswordPrefix + "/item-name"
const VersionAnnotation = OnepasswordPrefix + "/item-version"
const restartAnnotation = OnepasswordPrefix + "/last-restarted"
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
const ItemReferenceAnnotation = OnepasswordPrefix + "/item-reference"
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable")
var log = logf.Log
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string) error {
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string) error {
itemVersion := fmt.Sprint(item.Version)
// If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map
if secretAnnotations == nil {
secretAnnotations = map[string]string{}
annotations := map[string]string{
VersionAnnotation: itemVersion,
ItemReferenceAnnotation: fmt.Sprintf("op://%v/%v", item.Vault.ID, item.ID),
}
secretAnnotations[VersionAnnotation] = itemVersion
secretAnnotations[ItemPathAnnotation] = fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
if autoRestart != "" {
_, err := utils.StringToBool(autoRestart)
if err != nil {
log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName)
return err
}
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
annotations[RestartDeploymentsAnnotation] = autoRestart
}
// "Opaque" and "" secret types are treated the same by Kubernetes.
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item)
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, annotations, *item)
currentSecret := &corev1.Secret{}
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
@@ -68,17 +54,9 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return err
}
currentAnnotations := currentSecret.Annotations
currentLabels := currentSecret.Labels
currentSecretType := string(currentSecret.Type)
if !reflect.DeepEqual(currentSecretType, secretType) {
return ErrCannotUpdateSecretType
}
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) {
if currentSecret.Annotations[VersionAnnotation] != itemVersion {
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
currentSecret.ObjectMeta.Annotations = secretAnnotations
currentSecret.ObjectMeta.Labels = labels
currentSecret.ObjectMeta.Annotations = annotations
currentSecret.Data = secret.Data
return kubeClient.Update(context.Background(), currentSecret)
}
@@ -87,51 +65,31 @@ 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) *corev1.Secret {
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, item onepassword.Item) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: formatSecretName(name),
Namespace: namespace,
Annotations: annotations,
Labels: labels,
},
Data: BuildKubernetesSecretData(item.Fields, item.Files),
Type: corev1.SecretType(secretType),
Data: BuildKubernetesSecretData(item.Fields),
}
}
func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte {
func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byte {
secretData := map[string][]byte{}
for i := 0; i < len(fields); i++ {
if fields[i].Value != "" {
key := formatSecretDataName(fields[i].Label)
key := formatSecretName(fields[i].Label)
secretData[key] = []byte(fields[i].Value)
}
}
// populate unpopulated fields from files
for _, file := range files {
content, err := file.Content()
if err != nil {
log.Error(err, "Could not load contents of file %s", file.Name)
continue
}
if content != nil {
key := file.Name
if secretData[key] == nil {
secretData[key] = content
} else {
log.Info(fmt.Sprintf("File '%s' ignored because of a field with the same name", file.Name))
}
}
}
return secretData
}
// formatSecretName rewrites a value to be a valid Secret name.
// formatSecretName rewrites a value to be a valid Secret name or Secret data key.
//
// The Secret meta.name and data keys must be valid DNS subdomain names
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
// The Secret meta.name and data keys must be valid DNS subdomain names (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
func formatSecretName(value string) string {
if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 {
return value
@@ -139,18 +97,7 @@ func formatSecretName(value string) string {
return createValidSecretName(value)
}
// formatSecretDataName rewrites a value to be a valid Secret data key.
//
// The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.`
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
func formatSecretDataName(value string) string {
if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 {
return value
}
return createValidSecretDataName(value)
}
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+")
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-]+")
func createValidSecretName(value string) string {
result := strings.ToLower(value)
@@ -161,19 +108,5 @@ func createValidSecretName(value string) string {
}
// first and last character MUST be alphanumeric
return strings.Trim(result, "-.")
}
var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+")
var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)")
func createValidSecretDataName(value string) string {
result := invalidStartEndChars.ReplaceAllString(value, "")
result = invalidDataChars.ReplaceAllString(result, "-")
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
}
return result
return strings.Trim(result, "-")
}

View File

@@ -6,10 +6,11 @@ import (
"strings"
"testing"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
@@ -31,13 +32,7 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient()
secretLabels := map[string]string{}
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
secretType := ""
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -48,11 +43,7 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
t.Errorf("Secret was not created: %v", err)
}
compareFields(item.Fields, createdSecret.Data, t)
compareAnnotationsToItem(createdSecret.Annotations, item, t)
if createdSecret.Annotations["testAnnotation"] != "exists" {
t.Errorf("Expected testAnnotation to be merged with existing annotations, but wasn't.")
}
compareAnnotationsToItem(item.Vault.ID, item.ID, createdSecret.Annotations, item, t)
}
func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
@@ -66,12 +57,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient()
secretLabels := map[string]string{}
secretAnnotations := map[string]string{}
secretType := ""
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -82,7 +68,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
newItem.Version = 456
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -93,12 +79,12 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
t.Errorf("Secret was not found: %v", err)
}
compareFields(newItem.Fields, updatedSecret.Data, t)
compareAnnotationsToItem(updatedSecret.Annotations, newItem, t)
compareAnnotationsToItem(newItem.Vault.ID, newItem.ID, updatedSecret.Annotations, newItem, t)
}
func TestBuildKubernetesSecretData(t *testing.T) {
fields := generateFields(5)
secretData := BuildKubernetesSecretData(fields, nil)
secretData := BuildKubernetesSecretData(fields)
if len(secretData) != len(fields) {
t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData))
}
@@ -115,10 +101,8 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
}
item := onepassword.Item{}
item.Fields = generateFields(5)
labels := map[string]string{}
secretType := ""
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item)
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item)
if kubeSecret.Name != strings.ToLower(name) {
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
}
@@ -138,9 +122,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
annotations := map[string]string{
"annotationKey": "annotationValue",
}
labels := map[string]string{}
item := onepassword.Item{}
secretType := ""
item.Fields = []*onepassword.ItemField{
{
@@ -153,7 +135,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
},
}
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item)
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item)
// Assert Secret's meta.name was fixed
if kubeSecret.Name != expectedName {
@@ -171,44 +153,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
}
}
func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
secretName := "tls-test-secret-name"
namespace := "test"
item := onepassword.Item{}
item.Fields = generateFields(5)
item.Version = 123
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient()
secretLabels := map[string]string{}
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
secretType := "kubernetes.io/tls"
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
createdSecret := &corev1.Secret{}
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
if err != nil {
t.Errorf("Secret was not created: %v", err)
}
if createdSecret.Type != corev1.SecretTypeTLS {
t.Errorf("Expected secretType to be of tyype corev1.SecretTypeTLS, got %s", string(createdSecret.Type))
}
}
func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) {
actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
if err != nil {
t.Errorf("Was unable to parse Item Path")
}
func compareAnnotationsToItem(actualVaultId, actualItemId string, annotations map[string]string, item onepassword.Item, t *testing.T) {
if actualVaultId != item.Vault.ID {
t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId)
}
@@ -248,16 +193,8 @@ func generateFields(numToGenerate int) []*onepassword.ItemField {
return fields
}
func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
splitPath := strings.Split(path, "/")
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
return splitPath[1], splitPath[3], nil
}
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
}
func validLabel(v string) bool {
if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 {
if err := kubeValidate.IsDNS1123Subdomain(v); len(err) > 0 {
return false
}
return true

View File

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

View File

@@ -9,7 +9,7 @@ import (
const (
OnepasswordPrefix = "operator.1password.io"
ItemPathAnnotation = OnepasswordPrefix + "/item-path"
ItemReferenceAnnotation = OnepasswordPrefix + "/item-reference"
NameAnnotation = OnepasswordPrefix + "/item-name"
VersionAnnotation = OnepasswordPrefix + "/item-version"
RestartAnnotation = OnepasswordPrefix + "/last-restarted"

View File

@@ -22,7 +22,7 @@ func TestFilterAnnotations(t *testing.T) {
if len(filteredAnnotations) != 2 {
t.Errorf("Unexpected number of filtered annotations returned. Expected 2, got %v", len(filteredAnnotations))
}
_, found := filteredAnnotations[ItemPathAnnotation]
_, found := filteredAnnotations[ItemReferenceAnnotation]
if !found {
t.Errorf("One Password Annotation was filtered when it should not have been")
}
@@ -87,7 +87,7 @@ func TestGetNoAnnotationsForDeployment(t *testing.T) {
func getValidAnnotations() map[string]string {
return map[string]string{
ItemPathAnnotation: "vaults/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f/items/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f",
NameAnnotation: "secretName",
ItemReferenceAnnotation: "op://b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f",
NameAnnotation: "secretName",
}
}

View File

@@ -1,8 +1,6 @@
package onepassword
import (
corev1 "k8s.io/api/core/v1"
)
import corev1 "k8s.io/api/core/v1"
func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool {
for i := 0; i < len(containers); i++ {
@@ -15,15 +13,6 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string
}
}
}
envFromVariables := containers[i].EnvFrom
for j := 0; j < len(envFromVariables); j++ {
if envFromVariables[j].SecretRef != nil {
_, ok := secrets[envFromVariables[j].SecretRef.Name]
if ok {
return true
}
}
}
}
return false
}
@@ -39,15 +28,6 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st
}
}
}
envFromVariables := containers[i].EnvFrom
for j := 0; j < len(envFromVariables); j++ {
if envFromVariables[j].SecretRef != nil {
secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name]
if ok {
updatedDeploymentSecrets[secret.Name] = secret
}
}
}
}
return updatedDeploymentSecrets
}

View File

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

View File

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

View File

@@ -11,11 +11,16 @@ import (
var logger = logf.Log.WithName("retrieve_item")
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
const (
secretReferencePrefix = "op://"
)
func GetOnePasswordItemByReference(opConnectClient connect.Client, reference string) (*onepassword.Item, error) {
vaultValue, itemValue, err := ParseReference(reference)
if err != nil {
return nil, err
}
vaultId, err := getVaultId(opConnectClient, vaultValue)
if err != nil {
return nil, err
@@ -30,23 +35,31 @@ func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*one
if err != nil {
return nil, err
}
for _, file := range item.Files {
_, err := opConnectClient.GetFileContent(file)
if err != nil {
return nil, err
}
}
return item, nil
}
func ParseVaultAndItemFromPath(path string) (string, string, error) {
splitPath := strings.Split(path, "/")
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
return splitPath[1], splitPath[3], nil
func ParseReference(reference string) (string, string, error) {
if !strings.HasPrefix(reference, secretReferencePrefix) {
return "", "", fmt.Errorf("secret reference should start with `op://`")
}
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)
path := strings.TrimPrefix(reference, secretReferencePrefix)
splitPath := strings.Split(path, "/")
if len(splitPath) != 2 {
return "", "", fmt.Errorf("Invalid secret reference : %s. Secret references should match op://<vault>/<item>", reference)
}
vault := splitPath[0]
if vault == "" {
return "", "", fmt.Errorf("Invalid secret reference : %s. Vault can't be empty.", reference)
}
item := splitPath[1]
if item == "" {
return "", "", fmt.Errorf("Invalid secret reference : %s. Item can't be empty.", reference)
}
return vault, item, nil
}
func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {

View File

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

View File

@@ -110,16 +110,15 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
for i := 0; i < len(secrets.Items); i++ {
secret := secrets.Items[i]
itemPath := secret.Annotations[ItemPathAnnotation]
itemReference := secret.Annotations[ItemReferenceAnnotation]
currentVersion := secret.Annotations[VersionAnnotation]
if len(itemPath) == 0 || len(currentVersion) == 0 {
if len(itemReference) == 0 || len(currentVersion) == 0 {
continue
}
item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation])
item, err := GetOnePasswordItemByReference(h.opConnectClient, secret.Annotations[ItemReferenceAnnotation])
if err != nil {
log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name)
continue
return nil, fmt.Errorf("Failed to retrieve item: %v", err)
}
itemVersion := fmt.Sprint(item.Version)
@@ -132,7 +131,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
}
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
secret.Annotations[VersionAnnotation] = itemVersion
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, string(secret.Type), *item)
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item)
h.client.Update(context.Background(), updatedSecret)
if updatedSecrets[secret.Namespace] == nil {
updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)

View File

@@ -51,7 +51,7 @@ var (
"password": []byte(password),
"username": []byte(username),
}
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
itemReference = fmt.Sprintf("op://%v/%v", vaultId, itemId)
)
var defaultNamespace = &corev1.Namespace{
@@ -73,8 +73,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
NameAnnotation: "unlrelated secret",
ItemPathAnnotation: itemPath,
NameAnnotation: "unlrelated secret",
ItemReferenceAnnotation: itemReference,
},
},
},
@@ -83,8 +83,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -95,8 +95,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -149,8 +149,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -161,8 +161,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -186,8 +186,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
ItemPathAnnotation: itemPath,
NameAnnotation: name,
ItemReferenceAnnotation: itemReference,
NameAnnotation: name,
},
},
},
@@ -196,8 +196,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -208,8 +208,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -255,8 +255,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -267,8 +267,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -292,8 +292,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
ItemPathAnnotation: itemPath,
NameAnnotation: name,
ItemReferenceAnnotation: itemReference,
NameAnnotation: name,
},
},
},
@@ -302,8 +302,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -314,8 +314,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -369,8 +369,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -381,8 +381,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -439,7 +439,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
ItemReferenceAnnotation: itemReference,
RestartDeploymentsAnnotation: "true",
},
},
@@ -452,7 +452,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
ItemReferenceAnnotation: itemReference,
RestartDeploymentsAnnotation: "true",
},
},
@@ -510,7 +510,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
ItemReferenceAnnotation: itemReference,
RestartDeploymentsAnnotation: "false",
},
},
@@ -523,7 +523,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
ItemReferenceAnnotation: itemReference,
RestartDeploymentsAnnotation: "false",
},
},
@@ -580,8 +580,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -592,8 +592,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -657,8 +657,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -669,8 +669,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -730,8 +730,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: "old version",
ItemPathAnnotation: itemPath,
VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,
@@ -742,8 +742,8 @@ var tests = []testUpdateSecretTask{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion),
ItemPathAnnotation: itemPath,
VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference,
},
},
Data: expectedSecretData,

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -3,7 +3,6 @@ package connect
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -11,12 +10,11 @@ import (
"net/url"
"os"
"github.com/1Password/connect-sdk-go/onepassword"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
jaegerClientConfig "github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-client-go/zipkin"
"github.com/1Password/connect-sdk-go/onepassword"
)
const (
@@ -26,7 +24,6 @@ const (
// Client Represents an available 1Password Connect API to connect to
type Client interface {
GetVaults() ([]onepassword.Vault, error)
GetVault(uuid string) (*onepassword.Vault, error)
GetVaultsByTitle(uuid string) ([]onepassword.Vault, error)
GetItem(uuid string, vaultUUID string) (*onepassword.Item, error)
GetItems(vaultUUID string) ([]onepassword.Item, error)
@@ -35,8 +32,6 @@ type Client interface {
CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
DeleteItem(item *onepassword.Item, vaultUUID string) error
GetFile(fileUUID string, itemUUID string, vaultUUID string) (*onepassword.File, error)
GetFileContent(file *onepassword.File) ([]byte, error)
}
type httpClient interface {
@@ -117,41 +112,23 @@ func (rs *restClient) GetVaults() ([]onepassword.Vault, error) {
return nil, err
}
var vaults []onepassword.Vault
if err := parseResponse(response, http.StatusOK, &vaults); err != nil {
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unable to retrieve vaults. Receieved %q for %q", response.Status, vaultURL)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
vaults := []onepassword.Vault{}
if err := json.Unmarshal(body, &vaults); err != nil {
return nil, err
}
return vaults, nil
}
// GetVaults Get a list of all available vaults
func (rs *restClient) GetVault(uuid string) (*onepassword.Vault, error) {
if uuid == "" {
return nil, errors.New("no uuid provided")
}
span := rs.tracer.StartSpan("GetVault")
defer span.Finish()
vaultURL := fmt.Sprintf("/v1/vaults/%s", uuid)
request, err := rs.buildRequest(http.MethodGet, vaultURL, http.NoBody, span)
if err != nil {
return nil, err
}
response, err := rs.client.Do(request)
if err != nil {
return nil, err
}
var vault onepassword.Vault
if err := parseResponse(response, http.StatusOK, &vault); err != nil {
return nil, err
}
return &vault, nil
}
func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
span := rs.tracer.StartSpan("GetVaultsByTitle")
defer span.Finish()
@@ -168,8 +145,17 @@ func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error
return nil, err
}
var vaults []onepassword.Vault
if err := parseResponse(response, http.StatusOK, &vaults); err != nil {
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unable to retrieve vaults. Receieved %q for %q", response.Status, itemURL)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
vaults := []onepassword.Vault{}
if err := json.Unmarshal(body, &vaults); err != nil {
return nil, err
}
@@ -191,8 +177,18 @@ func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item,
if err != nil {
return nil, err
}
var item onepassword.Item
if err := parseResponse(response, http.StatusOK, &item); err != nil {
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
item := onepassword.Item{}
if err := json.Unmarshal(body, &item); err != nil {
return nil, err
}
@@ -230,8 +226,17 @@ func (rs *restClient) GetItemsByTitle(title string, vaultUUID string) ([]onepass
return nil, err
}
var items []onepassword.Item
if err := parseResponse(response, http.StatusOK, &items); err != nil {
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
items := []onepassword.Item{}
if err := json.Unmarshal(body, &items); err != nil {
return nil, err
}
@@ -253,8 +258,17 @@ func (rs *restClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
return nil, err
}
var items []onepassword.Item
if err := parseResponse(response, http.StatusOK, &items); err != nil {
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unable to retrieve items. Receieved %q for %q", response.Status, itemURL)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
items := []onepassword.Item{}
if err := json.Unmarshal(body, &items); err != nil {
return nil, err
}
@@ -282,8 +296,17 @@ func (rs *restClient) CreateItem(item *onepassword.Item, vaultUUID string) (*one
return nil, err
}
var newItem onepassword.Item
if err := parseResponse(response, http.StatusOK, &newItem); err != nil {
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unable to create item. Receieved %q for %q", response.Status, itemURL)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
newItem := onepassword.Item{}
if err := json.Unmarshal(body, &newItem); err != nil {
return nil, err
}
@@ -311,8 +334,17 @@ func (rs *restClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*one
return nil, err
}
var newItem onepassword.Item
if err := parseResponse(response, http.StatusOK, &newItem); err != nil {
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Unable to update item. Receieved %q for %q", response.Status, itemURL)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
newItem := onepassword.Item{}
if err := json.Unmarshal(body, &newItem); err != nil {
return nil, err
}
@@ -335,73 +367,13 @@ func (rs *restClient) DeleteItem(item *onepassword.Item, vaultUUID string) error
return err
}
if err := parseResponse(response, http.StatusNoContent, nil); err != nil {
return err
if response.StatusCode != http.StatusNoContent {
return fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL)
}
return nil
}
// GetFile Get a specific File in a specified item.
// This does not include the file contents. Call GetFileContent() to load the file's content.
func (rs *restClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) {
span := rs.tracer.StartSpan("GetFile")
defer span.Finish()
itemURL := fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s", vaultUUID, itemUUID, uuid)
request, err := rs.buildRequest(http.MethodGet, itemURL, http.NoBody, span)
if err != nil {
return nil, err
}
response, err := rs.client.Do(request)
if err != nil {
return nil, err
}
if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil {
return nil, err
}
var file onepassword.File
if err := parseResponse(response, http.StatusOK, &file); err != nil {
return nil, err
}
return &file, nil
}
// GetFileContent retrieves the file's content.
// If the file's content have previously been fetched, those contents are returned without making another request.
func (rs *restClient) GetFileContent(file *onepassword.File) ([]byte, error) {
if content, err := file.Content(); err == nil {
return content, nil
}
span := rs.tracer.StartSpan("GetFileContent")
defer span.Finish()
request, err := rs.buildRequest(http.MethodGet, file.ContentPath, http.NoBody, span)
if err != nil {
return nil, err
}
response, err := rs.client.Do(request)
if err != nil {
return nil, err
}
if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil {
return nil, err
}
content, err := readResponseBody(response, http.StatusOK)
if err != nil {
return nil, err
}
file.SetContent(content)
return content, nil
}
func (rs *restClient) buildRequest(method string, path string, body io.Reader, span opentracing.Span) (*http.Request, error) {
url := fmt.Sprintf("%s%s", rs.URL, path)
@@ -422,32 +394,3 @@ func (rs *restClient) buildRequest(method string, path string, body io.Reader, s
return request, nil
}
func parseResponse(resp *http.Response, expectedStatusCode int, result interface{}) error {
body, err := readResponseBody(resp, expectedStatusCode)
if err != nil {
return err
}
if result != nil {
if err := json.Unmarshal(body, result); err != nil {
return fmt.Errorf("decoding response: %s", err)
}
}
return nil
}
func readResponseBody(resp *http.Response, expectedStatusCode int) ([]byte, error) {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != expectedStatusCode {
var errResp *onepassword.Error
if err := json.Unmarshal(body, &errResp); err != nil {
return nil, fmt.Errorf("decoding error response: %s", err)
}
return nil, errResp
}
return body, nil
}

View File

@@ -1,104 +1,5 @@
package connect
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
)
// SDKVersion is the latest Semantic Version of the library
// Do not rename this variable without changing the regex in the Makefile
const SDKVersion = "1.2.0"
const VersionHeaderKey = "1Password-Connect-Version"
// expectMinimumConnectVersion returns an error if the provided minimum version for Connect is lower than the version
// reported in the response from Connect.
func expectMinimumConnectVersion(resp *http.Response, minimumVersion version) error {
serverVersion, err := getServerVersion(resp)
if err != nil {
// Return gracefully if server version cannot be determined reliably
return nil
}
if !serverVersion.IsGreaterOrEqualThan(minimumVersion) {
return fmt.Errorf("need at least version %s of Connect for this function, detected version %s. Please update your Connect server", minimumVersion, serverVersion)
}
return nil
}
func getServerVersion(resp *http.Response) (serverVersion, error) {
versionHeader := resp.Header.Get(VersionHeaderKey)
if versionHeader == "" {
// The last version without the version header was v1.2.0
return serverVersion{
version: version{1, 2, 0},
orEarlier: true,
}, nil
}
return parseServerVersion(versionHeader)
}
type version struct {
major int
minor int
patch int
}
// serverVersion describes the version reported by the server.
type serverVersion struct {
version
// orEarlier is true if the version is derived from the lack of a version header from the server.
orEarlier bool
}
func (v version) String() string {
return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch)
}
func (v serverVersion) String() string {
if v.orEarlier {
return v.version.String() + " (or earlier)"
}
return v.version.String()
}
// IsGreaterOrEqualThan returns true if the lefthand-side version is equal to or or a higher version than the provided
// minimum according to the semantic versioning rules.
func (v version) IsGreaterOrEqualThan(min version) bool {
if v.major != min.major {
// Different major version
return v.major > min.major
}
if v.minor != min.minor {
// Same major, but different minor version
return v.minor > min.minor
}
// Same major and minor version
return v.patch >= min.patch
}
func parseServerVersion(v string) (serverVersion, error) {
spl := strings.Split(v, ".")
if len(spl) != 3 {
return serverVersion{}, errors.New("wrong length")
}
var res [3]int
for i := range res {
tmp, err := strconv.Atoi(spl[i])
if err != nil {
return serverVersion{}, err
}
res[i] = tmp
}
return serverVersion{
version: version{
major: res[0],
minor: res[1],
patch: res[2],
},
}, nil
}
const SDKVersion = "1.0.1"

View File

@@ -1,21 +0,0 @@
package onepassword
import "fmt"
// Error is an error returned by the Connect API.
type Error struct {
StatusCode int `json:"status"`
Message string `json:"message"`
}
func (e *Error) Error() string {
return fmt.Sprintf("status %d: %s", e.StatusCode, e.Message)
}
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok {
return false
}
return t.Message == e.Message && t.StatusCode == e.StatusCode
}

View File

@@ -1,49 +0,0 @@
package onepassword
import (
"encoding/json"
"errors"
)
type File struct {
ID string `json:"id"`
Name string `json:"name"`
Section *ItemSection `json:"section,omitempty"`
Size int `json:"size"`
ContentPath string `json:"content_path"`
content []byte
}
func (f *File) UnmarshalJSON(data []byte) error {
var jsonFile struct {
ID string `json:"id"`
Name string `json:"name"`
Section *ItemSection `json:"section,omitempty"`
Size int `json:"size"`
ContentPath string `json:"content_path"`
Content []byte `json:"content,omitempty"`
}
if err := json.Unmarshal(data, &jsonFile); err != nil {
return err
}
f.ID = jsonFile.ID
f.Name = jsonFile.Name
f.Section = jsonFile.Section
f.Size = jsonFile.Size
f.ContentPath = jsonFile.ContentPath
f.content = jsonFile.Content
return nil
}
// 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

@@ -28,7 +28,6 @@ const (
Document ItemCategory = "DOCUMENT"
EmailAccount ItemCategory = "EMAIL_ACCOUNT"
SocialSecurityNumber ItemCategory = "SOCIAL_SECURITY_NUMBER"
ApiCredential ItemCategory = "API_CREDENTIAL"
Custom ItemCategory = "CUSTOM"
)
@@ -40,7 +39,7 @@ func (ic *ItemCategory) UnmarshalJSON(b []byte) error {
switch category {
case Login, Password, Server, Database, CreditCard, Membership, Passport, SoftwareLicense,
OutdoorLicense, SecureNote, WirelessRouter, BankAccount, DriverLicense, Identity, RewardProgram,
Document, EmailAccount, SocialSecurityNumber, ApiCredential:
Document, EmailAccount, SocialSecurityNumber:
*ic = category
default:
*ic = Custom
@@ -65,7 +64,6 @@ type Item struct {
Sections []*ItemSection `json:"sections,omitempty"`
Fields []*ItemField `json:"fields,omitempty"`
Files []*File `json:"files,omitempty"`
LastEditedBy string `json:"lastEditedBy,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`

View File

@@ -13,42 +13,12 @@ const (
compareGreater
)
var (
intType = reflect.TypeOf(int(1))
int8Type = reflect.TypeOf(int8(1))
int16Type = reflect.TypeOf(int16(1))
int32Type = reflect.TypeOf(int32(1))
int64Type = reflect.TypeOf(int64(1))
uintType = reflect.TypeOf(uint(1))
uint8Type = reflect.TypeOf(uint8(1))
uint16Type = reflect.TypeOf(uint16(1))
uint32Type = reflect.TypeOf(uint32(1))
uint64Type = reflect.TypeOf(uint64(1))
float32Type = reflect.TypeOf(float32(1))
float64Type = reflect.TypeOf(float64(1))
stringType = reflect.TypeOf("")
)
func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
obj1Value := reflect.ValueOf(obj1)
obj2Value := reflect.ValueOf(obj2)
// throughout this switch we try and avoid calling .Convert() if possible,
// as this has a pretty big performance impact
switch kind {
case reflect.Int:
{
intobj1, ok := obj1.(int)
if !ok {
intobj1 = obj1Value.Convert(intType).Interface().(int)
}
intobj2, ok := obj2.(int)
if !ok {
intobj2 = obj2Value.Convert(intType).Interface().(int)
}
intobj1 := obj1.(int)
intobj2 := obj2.(int)
if intobj1 > intobj2 {
return compareGreater, true
}
@@ -61,14 +31,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Int8:
{
int8obj1, ok := obj1.(int8)
if !ok {
int8obj1 = obj1Value.Convert(int8Type).Interface().(int8)
}
int8obj2, ok := obj2.(int8)
if !ok {
int8obj2 = obj2Value.Convert(int8Type).Interface().(int8)
}
int8obj1 := obj1.(int8)
int8obj2 := obj2.(int8)
if int8obj1 > int8obj2 {
return compareGreater, true
}
@@ -81,14 +45,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Int16:
{
int16obj1, ok := obj1.(int16)
if !ok {
int16obj1 = obj1Value.Convert(int16Type).Interface().(int16)
}
int16obj2, ok := obj2.(int16)
if !ok {
int16obj2 = obj2Value.Convert(int16Type).Interface().(int16)
}
int16obj1 := obj1.(int16)
int16obj2 := obj2.(int16)
if int16obj1 > int16obj2 {
return compareGreater, true
}
@@ -101,14 +59,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Int32:
{
int32obj1, ok := obj1.(int32)
if !ok {
int32obj1 = obj1Value.Convert(int32Type).Interface().(int32)
}
int32obj2, ok := obj2.(int32)
if !ok {
int32obj2 = obj2Value.Convert(int32Type).Interface().(int32)
}
int32obj1 := obj1.(int32)
int32obj2 := obj2.(int32)
if int32obj1 > int32obj2 {
return compareGreater, true
}
@@ -121,14 +73,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Int64:
{
int64obj1, ok := obj1.(int64)
if !ok {
int64obj1 = obj1Value.Convert(int64Type).Interface().(int64)
}
int64obj2, ok := obj2.(int64)
if !ok {
int64obj2 = obj2Value.Convert(int64Type).Interface().(int64)
}
int64obj1 := obj1.(int64)
int64obj2 := obj2.(int64)
if int64obj1 > int64obj2 {
return compareGreater, true
}
@@ -141,14 +87,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Uint:
{
uintobj1, ok := obj1.(uint)
if !ok {
uintobj1 = obj1Value.Convert(uintType).Interface().(uint)
}
uintobj2, ok := obj2.(uint)
if !ok {
uintobj2 = obj2Value.Convert(uintType).Interface().(uint)
}
uintobj1 := obj1.(uint)
uintobj2 := obj2.(uint)
if uintobj1 > uintobj2 {
return compareGreater, true
}
@@ -161,14 +101,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Uint8:
{
uint8obj1, ok := obj1.(uint8)
if !ok {
uint8obj1 = obj1Value.Convert(uint8Type).Interface().(uint8)
}
uint8obj2, ok := obj2.(uint8)
if !ok {
uint8obj2 = obj2Value.Convert(uint8Type).Interface().(uint8)
}
uint8obj1 := obj1.(uint8)
uint8obj2 := obj2.(uint8)
if uint8obj1 > uint8obj2 {
return compareGreater, true
}
@@ -181,14 +115,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Uint16:
{
uint16obj1, ok := obj1.(uint16)
if !ok {
uint16obj1 = obj1Value.Convert(uint16Type).Interface().(uint16)
}
uint16obj2, ok := obj2.(uint16)
if !ok {
uint16obj2 = obj2Value.Convert(uint16Type).Interface().(uint16)
}
uint16obj1 := obj1.(uint16)
uint16obj2 := obj2.(uint16)
if uint16obj1 > uint16obj2 {
return compareGreater, true
}
@@ -201,14 +129,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Uint32:
{
uint32obj1, ok := obj1.(uint32)
if !ok {
uint32obj1 = obj1Value.Convert(uint32Type).Interface().(uint32)
}
uint32obj2, ok := obj2.(uint32)
if !ok {
uint32obj2 = obj2Value.Convert(uint32Type).Interface().(uint32)
}
uint32obj1 := obj1.(uint32)
uint32obj2 := obj2.(uint32)
if uint32obj1 > uint32obj2 {
return compareGreater, true
}
@@ -221,14 +143,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Uint64:
{
uint64obj1, ok := obj1.(uint64)
if !ok {
uint64obj1 = obj1Value.Convert(uint64Type).Interface().(uint64)
}
uint64obj2, ok := obj2.(uint64)
if !ok {
uint64obj2 = obj2Value.Convert(uint64Type).Interface().(uint64)
}
uint64obj1 := obj1.(uint64)
uint64obj2 := obj2.(uint64)
if uint64obj1 > uint64obj2 {
return compareGreater, true
}
@@ -241,14 +157,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Float32:
{
float32obj1, ok := obj1.(float32)
if !ok {
float32obj1 = obj1Value.Convert(float32Type).Interface().(float32)
}
float32obj2, ok := obj2.(float32)
if !ok {
float32obj2 = obj2Value.Convert(float32Type).Interface().(float32)
}
float32obj1 := obj1.(float32)
float32obj2 := obj2.(float32)
if float32obj1 > float32obj2 {
return compareGreater, true
}
@@ -261,14 +171,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.Float64:
{
float64obj1, ok := obj1.(float64)
if !ok {
float64obj1 = obj1Value.Convert(float64Type).Interface().(float64)
}
float64obj2, ok := obj2.(float64)
if !ok {
float64obj2 = obj2Value.Convert(float64Type).Interface().(float64)
}
float64obj1 := obj1.(float64)
float64obj2 := obj2.(float64)
if float64obj1 > float64obj2 {
return compareGreater, true
}
@@ -281,14 +185,8 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
}
case reflect.String:
{
stringobj1, ok := obj1.(string)
if !ok {
stringobj1 = obj1Value.Convert(stringType).Interface().(string)
}
stringobj2, ok := obj2.(string)
if !ok {
stringobj2 = obj2Value.Convert(stringType).Interface().(string)
}
stringobj1 := obj1.(string)
stringobj2 := obj2.(string)
if stringobj1 > stringobj2 {
return compareGreater, true
}
@@ -342,24 +240,6 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter
return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs)
}
// Positive asserts that the specified element is positive
//
// assert.Positive(t, 1)
// assert.Positive(t, 1.23)
func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
zero := reflect.Zero(reflect.TypeOf(e))
return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs)
}
// Negative asserts that the specified element is negative
//
// assert.Negative(t, -1)
// assert.Negative(t, -1.23)
func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
zero := reflect.Zero(reflect.TypeOf(e))
return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs)
}
func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()

View File

@@ -114,24 +114,6 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) bool {
return Error(t, err, append([]interface{}{msg}, args...)...)
}
// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value.
// This is a wrapper for errors.As.
func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...)
}
// ErrorIsf asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return ErrorIs(t, err, target, append([]interface{}{msg}, args...)...)
}
// Eventuallyf asserts that given condition will be met in waitFor time,
// periodically checking target function each tick.
//
@@ -339,54 +321,6 @@ func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsil
return InEpsilonSlice(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...)
}
// IsDecreasingf asserts that the collection is decreasing
//
// assert.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted")
// assert.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted")
// assert.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted")
func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return IsDecreasing(t, object, append([]interface{}{msg}, args...)...)
}
// IsIncreasingf asserts that the collection is increasing
//
// assert.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted")
// assert.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted")
// assert.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted")
func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return IsIncreasing(t, object, append([]interface{}{msg}, args...)...)
}
// IsNonDecreasingf asserts that the collection is not decreasing
//
// assert.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted")
// assert.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted")
// assert.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted")
func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return IsNonDecreasing(t, object, append([]interface{}{msg}, args...)...)
}
// IsNonIncreasingf asserts that the collection is not increasing
//
// assert.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted")
// assert.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted")
// assert.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted")
func IsNonIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return IsNonIncreasing(t, object, append([]interface{}{msg}, args...)...)
}
// IsTypef asserts that the specified objects are of the same type.
func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
@@ -441,17 +375,6 @@ func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args .
return LessOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...)
}
// Negativef asserts that the specified element is negative
//
// assert.Negativef(t, -1, "error message %s", "formatted")
// assert.Negativef(t, -1.23, "error message %s", "formatted")
func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return Negative(t, e, append([]interface{}{msg}, args...)...)
}
// Neverf asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick.
//
@@ -553,15 +476,6 @@ func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg s
return NotEqualValues(t, expected, actual, append([]interface{}{msg}, args...)...)
}
// NotErrorIsf asserts that at none of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return NotErrorIs(t, err, target, append([]interface{}{msg}, args...)...)
}
// NotNilf asserts that the specified object is not nil.
//
// assert.NotNilf(t, err, "error message %s", "formatted")
@@ -658,17 +572,6 @@ func PanicsWithValuef(t TestingT, expected interface{}, f PanicTestFunc, msg str
return PanicsWithValue(t, expected, f, append([]interface{}{msg}, args...)...)
}
// Positivef asserts that the specified element is positive
//
// assert.Positivef(t, 1, "error message %s", "formatted")
// assert.Positivef(t, 1.23, "error message %s", "formatted")
func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return Positive(t, e, append([]interface{}{msg}, args...)...)
}
// Regexpf asserts that a specified regexp matches a string.
//
// assert.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted")

View File

@@ -204,42 +204,6 @@ func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool {
return Error(a.t, err, msgAndArgs...)
}
// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value.
// This is a wrapper for errors.As.
func (a *Assertions) ErrorAs(err error, target interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorAs(a.t, err, target, msgAndArgs...)
}
// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value.
// This is a wrapper for errors.As.
func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorAsf(a.t, err, target, msg, args...)
}
// ErrorIs asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorIs(a.t, err, target, msgAndArgs...)
}
// ErrorIsf asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorIsf(a.t, err, target, msg, args...)
}
// Errorf asserts that a function returned an error (i.e. not `nil`).
//
// actualObj, err := SomeFunction()
@@ -667,102 +631,6 @@ func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilo
return InEpsilonf(a.t, expected, actual, epsilon, msg, args...)
}
// IsDecreasing asserts that the collection is decreasing
//
// a.IsDecreasing([]int{2, 1, 0})
// a.IsDecreasing([]float{2, 1})
// a.IsDecreasing([]string{"b", "a"})
func (a *Assertions) IsDecreasing(object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsDecreasing(a.t, object, msgAndArgs...)
}
// IsDecreasingf asserts that the collection is decreasing
//
// a.IsDecreasingf([]int{2, 1, 0}, "error message %s", "formatted")
// a.IsDecreasingf([]float{2, 1}, "error message %s", "formatted")
// a.IsDecreasingf([]string{"b", "a"}, "error message %s", "formatted")
func (a *Assertions) IsDecreasingf(object interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsDecreasingf(a.t, object, msg, args...)
}
// IsIncreasing asserts that the collection is increasing
//
// a.IsIncreasing([]int{1, 2, 3})
// a.IsIncreasing([]float{1, 2})
// a.IsIncreasing([]string{"a", "b"})
func (a *Assertions) IsIncreasing(object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsIncreasing(a.t, object, msgAndArgs...)
}
// IsIncreasingf asserts that the collection is increasing
//
// a.IsIncreasingf([]int{1, 2, 3}, "error message %s", "formatted")
// a.IsIncreasingf([]float{1, 2}, "error message %s", "formatted")
// a.IsIncreasingf([]string{"a", "b"}, "error message %s", "formatted")
func (a *Assertions) IsIncreasingf(object interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsIncreasingf(a.t, object, msg, args...)
}
// IsNonDecreasing asserts that the collection is not decreasing
//
// a.IsNonDecreasing([]int{1, 1, 2})
// a.IsNonDecreasing([]float{1, 2})
// a.IsNonDecreasing([]string{"a", "b"})
func (a *Assertions) IsNonDecreasing(object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsNonDecreasing(a.t, object, msgAndArgs...)
}
// IsNonDecreasingf asserts that the collection is not decreasing
//
// a.IsNonDecreasingf([]int{1, 1, 2}, "error message %s", "formatted")
// a.IsNonDecreasingf([]float{1, 2}, "error message %s", "formatted")
// a.IsNonDecreasingf([]string{"a", "b"}, "error message %s", "formatted")
func (a *Assertions) IsNonDecreasingf(object interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsNonDecreasingf(a.t, object, msg, args...)
}
// IsNonIncreasing asserts that the collection is not increasing
//
// a.IsNonIncreasing([]int{2, 1, 1})
// a.IsNonIncreasing([]float{2, 1})
// a.IsNonIncreasing([]string{"b", "a"})
func (a *Assertions) IsNonIncreasing(object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsNonIncreasing(a.t, object, msgAndArgs...)
}
// IsNonIncreasingf asserts that the collection is not increasing
//
// a.IsNonIncreasingf([]int{2, 1, 1}, "error message %s", "formatted")
// a.IsNonIncreasingf([]float{2, 1}, "error message %s", "formatted")
// a.IsNonIncreasingf([]string{"b", "a"}, "error message %s", "formatted")
func (a *Assertions) IsNonIncreasingf(object interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsNonIncreasingf(a.t, object, msg, args...)
}
// IsType asserts that the specified objects are of the same type.
func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
@@ -871,28 +739,6 @@ func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...i
return Lessf(a.t, e1, e2, msg, args...)
}
// Negative asserts that the specified element is negative
//
// a.Negative(-1)
// a.Negative(-1.23)
func (a *Assertions) Negative(e interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return Negative(a.t, e, msgAndArgs...)
}
// Negativef asserts that the specified element is negative
//
// a.Negativef(-1, "error message %s", "formatted")
// a.Negativef(-1.23, "error message %s", "formatted")
func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return Negativef(a.t, e, msg, args...)
}
// Never asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick.
//
@@ -1095,24 +941,6 @@ func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg str
return NotEqualf(a.t, expected, actual, msg, args...)
}
// NotErrorIs asserts that at none of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return NotErrorIs(a.t, err, target, msgAndArgs...)
}
// NotErrorIsf asserts that at none of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return NotErrorIsf(a.t, err, target, msg, args...)
}
// NotNil asserts that the specified object is not nil.
//
// a.NotNil(err)
@@ -1305,28 +1133,6 @@ func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) b
return Panicsf(a.t, f, msg, args...)
}
// Positive asserts that the specified element is positive
//
// a.Positive(1)
// a.Positive(1.23)
func (a *Assertions) Positive(e interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return Positive(a.t, e, msgAndArgs...)
}
// Positivef asserts that the specified element is positive
//
// a.Positivef(1, "error message %s", "formatted")
// a.Positivef(1.23, "error message %s", "formatted")
func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return Positivef(a.t, e, msg, args...)
}
// Regexp asserts that a specified regexp matches a string.
//
// a.Regexp(regexp.MustCompile("start"), "it's starting")

View File

@@ -1,81 +0,0 @@
package assert
import (
"fmt"
"reflect"
)
// isOrdered checks that collection contains orderable elements.
func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool {
objKind := reflect.TypeOf(object).Kind()
if objKind != reflect.Slice && objKind != reflect.Array {
return false
}
objValue := reflect.ValueOf(object)
objLen := objValue.Len()
if objLen <= 1 {
return true
}
value := objValue.Index(0)
valueInterface := value.Interface()
firstValueKind := value.Kind()
for i := 1; i < objLen; i++ {
prevValue := value
prevValueInterface := valueInterface
value = objValue.Index(i)
valueInterface = value.Interface()
compareResult, isComparable := compare(prevValueInterface, valueInterface, firstValueKind)
if !isComparable {
return Fail(t, fmt.Sprintf("Can not compare type \"%s\" and \"%s\"", reflect.TypeOf(value), reflect.TypeOf(prevValue)), msgAndArgs...)
}
if !containsValue(allowedComparesResults, compareResult) {
return Fail(t, fmt.Sprintf(failMessage, prevValue, value), msgAndArgs...)
}
}
return true
}
// IsIncreasing asserts that the collection is increasing
//
// assert.IsIncreasing(t, []int{1, 2, 3})
// assert.IsIncreasing(t, []float{1, 2})
// assert.IsIncreasing(t, []string{"a", "b"})
func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs)
}
// IsNonIncreasing asserts that the collection is not increasing
//
// assert.IsNonIncreasing(t, []int{2, 1, 1})
// assert.IsNonIncreasing(t, []float{2, 1})
// assert.IsNonIncreasing(t, []string{"b", "a"})
func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs)
}
// IsDecreasing asserts that the collection is decreasing
//
// assert.IsDecreasing(t, []int{2, 1, 0})
// assert.IsDecreasing(t, []float{2, 1})
// assert.IsDecreasing(t, []string{"b", "a"})
func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs)
}
// IsNonDecreasing asserts that the collection is not decreasing
//
// assert.IsNonDecreasing(t, []int{1, 1, 2})
// assert.IsNonDecreasing(t, []float{1, 2})
// assert.IsNonDecreasing(t, []string{"a", "b"})
func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs)
}

View File

@@ -172,8 +172,8 @@ func isTest(name, prefix string) bool {
if len(name) == len(prefix) { // "Test" is ok
return true
}
r, _ := utf8.DecodeRuneInString(name[len(prefix):])
return !unicode.IsLower(r)
rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
return !unicode.IsLower(rune)
}
func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
@@ -1622,7 +1622,6 @@ var spewConfig = spew.ConfigState{
DisableCapacities: true,
SortKeys: true,
DisableMethods: true,
MaxDepth: 10,
}
type tHelper interface {
@@ -1694,81 +1693,3 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D
}
}
}
// ErrorIs asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if errors.Is(err, target) {
return true
}
var expectedText string
if target != nil {
expectedText = target.Error()
}
chain := buildErrorChainString(err)
return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+
"expected: %q\n"+
"in chain: %s", expectedText, chain,
), msgAndArgs...)
}
// NotErrorIs asserts that at none of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if !errors.Is(err, target) {
return true
}
var expectedText string
if target != nil {
expectedText = target.Error()
}
chain := buildErrorChainString(err)
return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+
"found: %q\n"+
"in chain: %s", expectedText, chain,
), msgAndArgs...)
}
// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value.
// This is a wrapper for errors.As.
func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if errors.As(err, target) {
return true
}
chain := buildErrorChainString(err)
return Fail(t, fmt.Sprintf("Should be in error chain:\n"+
"expected: %q\n"+
"in chain: %s", target, chain,
), msgAndArgs...)
}
func buildErrorChainString(err error) string {
if err == nil {
return ""
}
e := errors.Unwrap(err)
chain := fmt.Sprintf("%q", err.Error())
for e != nil {
chain += fmt.Sprintf("\n\t%q", e.Error())
e = errors.Unwrap(e)
}
return chain
}

4
vendor/modules.txt vendored
View File

@@ -1,6 +1,6 @@
# cloud.google.com/go v0.49.0
cloud.google.com/go/compute/metadata
# github.com/1Password/connect-sdk-go v1.2.0
# github.com/1Password/connect-sdk-go v1.0.1
github.com/1Password/connect-sdk-go/connect
github.com/1Password/connect-sdk-go/onepassword
# github.com/Azure/go-autorest/autorest v0.9.3-0.20191028180845-3492b2aff503
@@ -110,7 +110,7 @@ github.com/prometheus/procfs/internal/fs
github.com/prometheus/procfs/internal/util
# github.com/spf13/pflag v1.0.5
github.com/spf13/pflag
# github.com/stretchr/testify v1.7.0
# github.com/stretchr/testify v1.6.1
github.com/stretchr/testify/assert
# github.com/uber/jaeger-client-go v2.25.0+incompatible
github.com/uber/jaeger-client-go