diff --git a/.VERSION b/.VERSION index e6d5cb8..589268e 100644 --- a/.VERSION +++ b/.VERSION @@ -1 +1 @@ -1.0.2 \ No newline at end of file +1.3.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 79cee80..6fac99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,31 @@ --- +[//]: # (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 diff --git a/README.md b/README.md index ca2e7d3..f2a21df 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,13 @@ 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=1password-credentials.json +kubectl create secret generic op-credentials --from-file=op-session ``` Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`: @@ -53,12 +52,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=" +kubectl create secret generic onepassword-token --from-literal=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 op-k8s-operator --vault ) +kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token op-k8s-operator --vault ) ``` [More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens) @@ -68,13 +67,13 @@ $ kubectl create secret generic onepassword-token --from-literal=token=$(op crea 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** @@ -112,13 +111,13 @@ spec: Deploy the OnePasswordItem to Kubernetes: ```bash -$ kubectl apply -f .yaml +kubectl apply -f .yaml ``` To test that the Kubernetes Secret check that the following command returns a secret: ```bash -$ kubectl get secret +kubectl get secret ``` Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret. @@ -137,6 +136,11 @@ metadata: Applying this yaml file will create a Kubernetes Secret with the name `` and contents from the location specified at the specified Item Path. +The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password. +In case of fields that store files, the file's contents will be used as the value. + +Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence. + Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret. If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes. @@ -171,7 +175,7 @@ metadata: annotations: operator.1password.io/auto-restart: "true" ``` -If the value is not set, the auto reset settings on the operator will be used. This value can be overwritten by deployment. +If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment. **Per Deployment** This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below: @@ -184,7 +188,7 @@ metadata: annotations: operator.1password.io/auto-restart: "true" ``` -If the value is not set, the auto reset settings on the namespace will be used. +If the value is not set, the auto restart settings on the namespace will be used. **Per OnePasswordItem Custom Resource** 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: @@ -197,7 +201,7 @@ metadata: annotations: operator.1password.io/auto-restart: "true" ``` -If the value is not set, the auto reset settings on the deployment will be used. +If the value is not set, the auto restart settings on the deployment will be used. ## Development diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 2d7904c..05ca380 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -178,7 +178,10 @@ func main() { ticker.Stop() return case <-ticker.C: - updatedSecretsPoller.UpdateKubernetesSecretsTask() + err := updatedSecretsPoller.UpdateKubernetesSecretsTask() + if err != nil { + log.Error(err, "error running update kubernetes secret task") + } } } }() diff --git a/deploy/crds/onepassword.com_onepassworditems_crd.yaml b/deploy/crds/onepassword.com_onepassworditems_crd.yaml index 3a21918..2a8dc9e 100644 --- a/deploy/crds/onepassword.com_onepassworditems_crd.yaml +++ b/deploy/crds/onepassword.com_onepassworditems_crd.yaml @@ -39,4 +39,7 @@ spec: status: description: OnePasswordItemStatus defines the observed state of OnePasswordItem type: object + type: + description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types' + type: string type: object diff --git a/go.mod b/go.mod index 4327e11..920c69a 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/1Password/onepassword-operator go 1.13 require ( - github.com/1Password/connect-sdk-go v1.0.1 + github.com/1Password/connect-sdk-go v1.2.0 github.com/operator-framework/operator-sdk v0.19.0 github.com/prometheus/common v0.14.0 // indirect github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.6.1 + github.com/stretchr/testify v1.7.0 k8s.io/api v0.18.2 k8s.io/apimachinery v0.18.2 k8s.io/client-go v12.0.0+incompatible diff --git a/go.sum b/go.sum index 470bc3f..bf98094 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ 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= @@ -885,6 +887,8 @@ 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= diff --git a/pkg/apis/onepassword/v1/onepasswordsecret_types.go b/pkg/apis/onepassword/v1/onepasswordsecret_types.go index 7918539..a2e9ba3 100644 --- a/pkg/apis/onepassword/v1/onepasswordsecret_types.go +++ b/pkg/apis/onepassword/v1/onepasswordsecret_types.go @@ -26,6 +26,7 @@ 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"` diff --git a/pkg/controller/deployment/deployment_controller.go b/pkg/controller/deployment/deployment_controller.go index 3b81fdf..fc2183d 100644 --- a/pkg/controller/deployment/deployment_controller.go +++ b/pkg/controller/deployment/deployment_controller.go @@ -192,6 +192,8 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat secretName := annotations[op.NameAnnotation] secretLabels := map[string]string(nil) + secretType := "" + if len(secretName) == 0 { reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.") return nil @@ -202,5 +204,5 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat return fmt.Errorf("Failed to retrieve item: %v", err) } - return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, annotations) + return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, annotations) } diff --git a/pkg/controller/deployment/deployment_controller_test.go b/pkg/controller/deployment/deployment_controller_test.go index 25ae956..bd98231 100644 --- a/pkg/controller/deployment/deployment_controller_test.go +++ b/pkg/controller/deployment/deployment_controller_test.go @@ -279,7 +279,7 @@ var tests = []testReconcileItem{ Name: name, Namespace: namespace, Annotations: map[string]string{ - op.VersionAnnotation: fmt.Sprint(version), + op.VersionAnnotation: fmt.Sprint(version), op.ItemPathAnnotation: itemPath, op.NameAnnotation: name, }, @@ -292,7 +292,7 @@ var tests = []testReconcileItem{ Name: name, Namespace: namespace, Annotations: map[string]string{ - op.VersionAnnotation: fmt.Sprint(version), + op.VersionAnnotation: fmt.Sprint(version), op.ItemPathAnnotation: itemPath, op.NameAnnotation: name, }, @@ -329,6 +329,7 @@ var tests = []testReconcileItem{ op.VersionAnnotation: "456", }, }, + Type: corev1.SecretType(""), Data: expectedSecretData, }, expectedError: nil, @@ -340,6 +341,7 @@ var tests = []testReconcileItem{ op.VersionAnnotation: fmt.Sprint(version), }, }, + Type: corev1.SecretType(""), Data: expectedSecretData, }, opItem: map[string]string{ @@ -373,6 +375,7 @@ var tests = []testReconcileItem{ op.VersionAnnotation: fmt.Sprint(version), }, }, + Type: corev1.SecretType(""), Data: expectedSecretData, }, opItem: map[string]string{ diff --git a/pkg/controller/onepassworditem/onepassworditem_controller.go b/pkg/controller/onepassworditem/onepassworditem_controller.go index dc7fdff..0788f02 100644 --- a/pkg/controller/onepassworditem/onepassworditem_controller.go +++ b/pkg/controller/onepassworditem/onepassworditem_controller.go @@ -3,6 +3,7 @@ package onepassworditem import ( "context" "fmt" + onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" "github.com/1Password/onepassword-operator/pkg/onepassword" @@ -145,6 +146,7 @@ func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1 secretName := resource.GetName() labels := resource.Labels annotations := resource.Annotations + secretType := resource.Type autoRestart := annotations[op.RestartDeploymentsAnnotation] item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath) @@ -152,5 +154,5 @@ func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1 return fmt.Errorf("Failed to retrieve item: %v", err) } - return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, annotations) + return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, secretType, annotations) } diff --git a/pkg/controller/onepassworditem/onepassworditem_test.go b/pkg/controller/onepassworditem/onepassworditem_test.go index 85915a2..d56c224 100644 --- a/pkg/controller/onepassworditem/onepassworditem_test.go +++ b/pkg/controller/onepassworditem/onepassworditem_test.go @@ -5,6 +5,7 @@ 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" @@ -119,7 +120,7 @@ var tests = []testReconcileItem{ Name: name, Namespace: namespace, Annotations: map[string]string{ - op.VersionAnnotation: fmt.Sprint(version), + op.VersionAnnotation: fmt.Sprint(version), op.ItemPathAnnotation: itemPath, }, }, @@ -131,7 +132,7 @@ var tests = []testReconcileItem{ Name: name, Namespace: namespace, Annotations: map[string]string{ - op.VersionAnnotation: fmt.Sprint(version), + op.VersionAnnotation: fmt.Sprint(version), op.ItemPathAnnotation: itemPath, }, }, @@ -153,7 +154,7 @@ var tests = []testReconcileItem{ Name: name, Namespace: namespace, Annotations: map[string]string{ - op.VersionAnnotation: fmt.Sprint(version), + op.VersionAnnotation: fmt.Sprint(version), op.ItemPathAnnotation: itemPath, }, Labels: map[string]string{}, @@ -167,7 +168,7 @@ var tests = []testReconcileItem{ Name: name, Namespace: namespace, Annotations: map[string]string{ - op.VersionAnnotation: "456", + op.VersionAnnotation: "456", op.ItemPathAnnotation: itemPath, }, Labels: map[string]string{}, @@ -180,7 +181,7 @@ var tests = []testReconcileItem{ Name: name, Namespace: namespace, Annotations: map[string]string{ - op.VersionAnnotation: fmt.Sprint(version), + op.VersionAnnotation: fmt.Sprint(version), op.ItemPathAnnotation: itemPath, }, Labels: map[string]string{}, @@ -192,6 +193,59 @@ 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{ @@ -206,6 +260,7 @@ var tests = []testReconcileItem{ Spec: onepasswordv1.OnePasswordItemSpec{ ItemPath: itemPath, }, + Type: "custom", }, existingSecret: nil, expectedError: nil, @@ -217,6 +272,51 @@ 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{ diff --git a/pkg/kubernetessecrets/kubernetes_secrets_builder.go b/pkg/kubernetessecrets/kubernetes_secrets_builder.go index 48e80fa..3415737 100644 --- a/pkg/kubernetessecrets/kubernetes_secrets_builder.go +++ b/pkg/kubernetessecrets/kubernetes_secrets_builder.go @@ -7,13 +7,17 @@ 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" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "reflect" kubeValidate "k8s.io/apimachinery/pkg/util/validation" kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" @@ -27,9 +31,11 @@ const restartAnnotation = OnepasswordPrefix + "/last-restarted" const ItemPathAnnotation = OnepasswordPrefix + "/item-path" const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" +var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable") + var log = logf.Log -func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretAnnotations map[string]string) error { +func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string) error { itemVersion := fmt.Sprint(item.Version) @@ -49,7 +55,9 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa } secretAnnotations[RestartDeploymentsAnnotation] = autoRestart } - secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, *item) + + // "Opaque" and "" secret types are treated the same by Kubernetes. + secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item) currentSecret := &corev1.Secret{} err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) @@ -60,7 +68,14 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa return err } - if ! reflect.DeepEqual(currentSecret.Annotations, secretAnnotations) || ! reflect.DeepEqual(currentSecret.Labels, labels) { + 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) { log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) currentSecret.ObjectMeta.Annotations = secretAnnotations currentSecret.ObjectMeta.Labels = labels @@ -72,7 +87,7 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa return nil } -func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, item onepassword.Item) *corev1.Secret { +func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item) *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: formatSecretName(name), @@ -80,11 +95,12 @@ func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotation Annotations: annotations, Labels: labels, }, - Data: BuildKubernetesSecretData(item.Fields), + Data: BuildKubernetesSecretData(item.Fields, item.Files), + Type: corev1.SecretType(secretType), } } -func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byte { +func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte { secretData := map[string][]byte{} for i := 0; i < len(fields); i++ { if fields[i].Value != "" { @@ -92,6 +108,23 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byt 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 } diff --git a/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go b/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go index 63c0d0c..2bc9faf 100644 --- a/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go +++ b/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go @@ -35,7 +35,9 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { secretAnnotations := map[string]string{ "testAnnotation": "exists", } - err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations) + secretType := "" + + err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations) if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -66,7 +68,10 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { kubeClient := fake.NewFakeClient() secretLabels := map[string]string{} secretAnnotations := map[string]string{} - err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations) + secretType := "" + + err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations) + if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -77,7 +82,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { newItem.Version = 456 newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" - err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretAnnotations) + err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations) if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -93,7 +98,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { func TestBuildKubernetesSecretData(t *testing.T) { fields := generateFields(5) - secretData := BuildKubernetesSecretData(fields) + secretData := BuildKubernetesSecretData(fields, nil) if len(secretData) != len(fields) { t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData)) } @@ -111,8 +116,9 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { item := onepassword.Item{} item.Fields = generateFields(5) labels := map[string]string{} + secretType := "" - kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item) + kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item) if kubeSecret.Name != strings.ToLower(name) { t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) } @@ -134,6 +140,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) { } labels := map[string]string{} item := onepassword.Item{} + secretType := "" item.Fields = []*onepassword.ItemField{ { @@ -146,7 +153,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) { }, } - kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item) + kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item) // Assert Secret's meta.name was fixed if kubeSecret.Name != expectedName { @@ -164,6 +171,39 @@ 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 { diff --git a/pkg/mocks/mocksecretserver.go b/pkg/mocks/mocksecretserver.go index 489182f..29c7664 100644 --- a/pkg/mocks/mocksecretserver.go +++ b/pkg/mocks/mocksecretserver.go @@ -7,6 +7,7 @@ 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) @@ -14,11 +15,14 @@ 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) @@ -26,6 +30,8 @@ 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 @@ -37,6 +43,10 @@ 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) } @@ -64,3 +74,11 @@ 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) +} diff --git a/pkg/onepassword/containers.go b/pkg/onepassword/containers.go index 1f51bd9..c0910a8 100644 --- a/pkg/onepassword/containers.go +++ b/pkg/onepassword/containers.go @@ -1,6 +1,8 @@ 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++ { @@ -13,6 +15,15 @@ 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 } @@ -28,6 +39,15 @@ 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 } diff --git a/pkg/onepassword/containers_test.go b/pkg/onepassword/containers_test.go index 676c517..c6540f9 100644 --- a/pkg/onepassword/containers_test.go +++ b/pkg/onepassword/containers_test.go @@ -4,9 +4,10 @@ import ( "testing" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestAreContainersUsingSecrets(t *testing.T) { +func TestAreContainersUsingSecretsFromEnv(t *testing.T) { secretNamesToSearch := map[string]*corev1.Secret{ "onepassword-database-secret": &corev1.Secret{}, "onepassword-api-key": &corev1.Secret{}, @@ -18,7 +19,26 @@ func TestAreContainersUsingSecrets(t *testing.T) { "some_other_key", } - containers := generateContainers(containerSecretNames) + containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) + + if !AreContainersUsingSecrets(containers, secretNamesToSearch) { + t.Errorf("Expected that containers were using secrets but they were not detected.") + } +} + +func TestAreContainersUsingSecretsFromEnvFrom(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": {}, + "onepassword-api-key": {}, + } + + containerSecretNames := []string{ + "onepassword-database-secret", + "onepassword-api-key", + "some_other_key", + } + + containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) if !AreContainersUsingSecrets(containers, secretNamesToSearch) { t.Errorf("Expected that containers were using secrets but they were not detected.") @@ -27,17 +47,39 @@ func TestAreContainersUsingSecrets(t *testing.T) { func TestAreContainersNotUsingSecrets(t *testing.T) { secretNamesToSearch := map[string]*corev1.Secret{ - "onepassword-database-secret": &corev1.Secret{}, - "onepassword-api-key": &corev1.Secret{}, + "onepassword-database-secret": {}, + "onepassword-api-key": {}, } containerSecretNames := []string{ "some_other_key", } - containers := generateContainers(containerSecretNames) + containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) if AreContainersUsingSecrets(containers, secretNamesToSearch) { t.Errorf("Expected that containers were not using secrets but they were detected.") } } + +func TestAppendUpdatedContainerSecretsParsesEnvFromEnv(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": {}, + "onepassword-api-key": {ObjectMeta: metav1.ObjectMeta{Name: "onepassword-api-key"}}, + } + + containerSecretNames := []string{ + "onepassword-api-key", + } + + containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) + + updatedDeploymentSecrets := map[string]*corev1.Secret{} + updatedDeploymentSecrets = AppendUpdatedContainerSecrets(containers, secretNamesToSearch, updatedDeploymentSecrets) + + secretKeyName := "onepassword-api-key" + + if updatedDeploymentSecrets[secretKeyName] != secretNamesToSearch[secretKeyName] { + t.Errorf("Expected that updated Secret from envfrom is found.") + } +} diff --git a/pkg/onepassword/deployments_test.go b/pkg/onepassword/deployments_test.go index d7445b1..45761fc 100644 --- a/pkg/onepassword/deployments_test.go +++ b/pkg/onepassword/deployments_test.go @@ -39,7 +39,7 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { } deployment := &appsv1.Deployment{} - deployment.Spec.Template.Spec.Containers = generateContainers(containerSecretNames) + deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames) if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { t.Errorf("Expected that deployment was using secrets but they were not detected.") } diff --git a/pkg/onepassword/items.go b/pkg/onepassword/items.go index 11c4914..c022f5d 100644 --- a/pkg/onepassword/items.go +++ b/pkg/onepassword/items.go @@ -30,6 +30,14 @@ 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 } diff --git a/pkg/onepassword/object_generators_for_test.go b/pkg/onepassword/object_generators_for_test.go index 7e2f526..2392070 100644 --- a/pkg/onepassword/object_generators_for_test.go +++ b/pkg/onepassword/object_generators_for_test.go @@ -17,8 +17,7 @@ func generateVolumes(names []string) []corev1.Volume { } return volumes } - -func generateContainers(names []string) []corev1.Container { +func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container { containers := []corev1.Container{} for i := 0; i < len(names); i++ { container := corev1.Container{ @@ -40,3 +39,16 @@ func generateContainers(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 +} diff --git a/pkg/onepassword/secret_update_handler.go b/pkg/onepassword/secret_update_handler.go index a51ac52..f83a139 100644 --- a/pkg/onepassword/secret_update_handler.go +++ b/pkg/onepassword/secret_update_handler.go @@ -3,9 +3,10 @@ package onepassword import ( "context" "fmt" - v1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" "time" + v1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" + kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" "github.com/1Password/onepassword-operator/pkg/utils" @@ -121,7 +122,8 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]* item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath) if err != nil { - return nil, fmt.Errorf("Failed to retrieve item: %v", err) + log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name) + continue } itemVersion := fmt.Sprint(item.Version) @@ -138,7 +140,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]* log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) secret.Annotations[VersionAnnotation] = itemVersion secret.Annotations[ItemPathAnnotation] = itemPathString - updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, *item) + updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, string(secret.Type), *item) log.Info(fmt.Sprintf("New secret path: %v and version: %v", updatedSecret.Annotations[ItemPathAnnotation], updatedSecret.Annotations[VersionAnnotation])) h.client.Update(context.Background(), updatedSecret) if updatedSecrets[secret.Namespace] == nil { diff --git a/vendor/github.com/1Password/connect-sdk-go/LICENSE b/vendor/github.com/1Password/connect-sdk-go/LICENSE new file mode 100644 index 0000000..340215c --- /dev/null +++ b/vendor/github.com/1Password/connect-sdk-go/LICENSE @@ -0,0 +1,21 @@ +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. diff --git a/vendor/github.com/1Password/connect-sdk-go/connect/client.go b/vendor/github.com/1Password/connect-sdk-go/connect/client.go index f26a402..16b1832 100644 --- a/vendor/github.com/1Password/connect-sdk-go/connect/client.go +++ b/vendor/github.com/1Password/connect-sdk-go/connect/client.go @@ -3,6 +3,7 @@ package connect import ( "bytes" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -10,11 +11,12 @@ 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 ( @@ -24,6 +26,7 @@ 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) @@ -32,6 +35,8 @@ 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 { @@ -112,23 +117,41 @@ func (rs *restClient) GetVaults() ([]onepassword.Vault, error) { return nil, err } - 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 { + var vaults []onepassword.Vault + if err := parseResponse(response, http.StatusOK, &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() @@ -145,17 +168,8 @@ func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error return nil, err } - 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 { + var vaults []onepassword.Vault + if err := parseResponse(response, http.StatusOK, &vaults); err != nil { return nil, err } @@ -177,18 +191,8 @@ func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, if err != nil { return nil, err } - - 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 { + var item onepassword.Item + if err := parseResponse(response, http.StatusOK, &item); err != nil { return nil, err } @@ -226,17 +230,8 @@ func (rs *restClient) GetItemsByTitle(title string, vaultUUID string) ([]onepass return nil, err } - 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 { + var items []onepassword.Item + if err := parseResponse(response, http.StatusOK, &items); err != nil { return nil, err } @@ -258,17 +253,8 @@ func (rs *restClient) GetItems(vaultUUID string) ([]onepassword.Item, error) { return nil, err } - 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 { + var items []onepassword.Item + if err := parseResponse(response, http.StatusOK, &items); err != nil { return nil, err } @@ -296,17 +282,8 @@ func (rs *restClient) CreateItem(item *onepassword.Item, vaultUUID string) (*one return nil, err } - 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 { + var newItem onepassword.Item + if err := parseResponse(response, http.StatusOK, &newItem); err != nil { return nil, err } @@ -334,17 +311,8 @@ func (rs *restClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*one return nil, err } - 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 { + var newItem onepassword.Item + if err := parseResponse(response, http.StatusOK, &newItem); err != nil { return nil, err } @@ -367,13 +335,73 @@ func (rs *restClient) DeleteItem(item *onepassword.Item, vaultUUID string) error return err } - if response.StatusCode != http.StatusNoContent { - return fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL) + if err := parseResponse(response, http.StatusNoContent, nil); err != nil { + return err } 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) @@ -394,3 +422,32 @@ 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 +} diff --git a/vendor/github.com/1Password/connect-sdk-go/connect/version.go b/vendor/github.com/1Password/connect-sdk-go/connect/version.go index 61f9195..b654f8b 100644 --- a/vendor/github.com/1Password/connect-sdk-go/connect/version.go +++ b/vendor/github.com/1Password/connect-sdk-go/connect/version.go @@ -1,5 +1,104 @@ 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.0.1" +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 +} diff --git a/vendor/github.com/1Password/connect-sdk-go/onepassword/errors.go b/vendor/github.com/1Password/connect-sdk-go/onepassword/errors.go new file mode 100644 index 0000000..329a0b3 --- /dev/null +++ b/vendor/github.com/1Password/connect-sdk-go/onepassword/errors.go @@ -0,0 +1,21 @@ +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 +} diff --git a/vendor/github.com/1Password/connect-sdk-go/onepassword/files.go b/vendor/github.com/1Password/connect-sdk-go/onepassword/files.go new file mode 100644 index 0000000..a34692f --- /dev/null +++ b/vendor/github.com/1Password/connect-sdk-go/onepassword/files.go @@ -0,0 +1,49 @@ +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 +} diff --git a/vendor/github.com/1Password/connect-sdk-go/onepassword/items.go b/vendor/github.com/1Password/connect-sdk-go/onepassword/items.go index 71bc409..783ca8e 100644 --- a/vendor/github.com/1Password/connect-sdk-go/onepassword/items.go +++ b/vendor/github.com/1Password/connect-sdk-go/onepassword/items.go @@ -28,6 +28,7 @@ const ( Document ItemCategory = "DOCUMENT" EmailAccount ItemCategory = "EMAIL_ACCOUNT" SocialSecurityNumber ItemCategory = "SOCIAL_SECURITY_NUMBER" + ApiCredential ItemCategory = "API_CREDENTIAL" Custom ItemCategory = "CUSTOM" ) @@ -39,7 +40,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: + Document, EmailAccount, SocialSecurityNumber, ApiCredential: *ic = category default: *ic = Custom @@ -64,6 +65,7 @@ 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"` diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go index dc20039..41649d2 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_compare.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare.go @@ -13,12 +13,42 @@ 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 := obj1.(int) - intobj2 := obj2.(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) + } if intobj1 > intobj2 { return compareGreater, true } @@ -31,8 +61,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Int8: { - int8obj1 := obj1.(int8) - int8obj2 := obj2.(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) + } if int8obj1 > int8obj2 { return compareGreater, true } @@ -45,8 +81,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Int16: { - int16obj1 := obj1.(int16) - int16obj2 := obj2.(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) + } if int16obj1 > int16obj2 { return compareGreater, true } @@ -59,8 +101,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Int32: { - int32obj1 := obj1.(int32) - int32obj2 := obj2.(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) + } if int32obj1 > int32obj2 { return compareGreater, true } @@ -73,8 +121,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Int64: { - int64obj1 := obj1.(int64) - int64obj2 := obj2.(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) + } if int64obj1 > int64obj2 { return compareGreater, true } @@ -87,8 +141,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Uint: { - uintobj1 := obj1.(uint) - uintobj2 := obj2.(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) + } if uintobj1 > uintobj2 { return compareGreater, true } @@ -101,8 +161,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Uint8: { - uint8obj1 := obj1.(uint8) - uint8obj2 := obj2.(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) + } if uint8obj1 > uint8obj2 { return compareGreater, true } @@ -115,8 +181,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Uint16: { - uint16obj1 := obj1.(uint16) - uint16obj2 := obj2.(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) + } if uint16obj1 > uint16obj2 { return compareGreater, true } @@ -129,8 +201,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Uint32: { - uint32obj1 := obj1.(uint32) - uint32obj2 := obj2.(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) + } if uint32obj1 > uint32obj2 { return compareGreater, true } @@ -143,8 +221,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Uint64: { - uint64obj1 := obj1.(uint64) - uint64obj2 := obj2.(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) + } if uint64obj1 > uint64obj2 { return compareGreater, true } @@ -157,8 +241,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Float32: { - float32obj1 := obj1.(float32) - float32obj2 := obj2.(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) + } if float32obj1 > float32obj2 { return compareGreater, true } @@ -171,8 +261,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.Float64: { - float64obj1 := obj1.(float64) - float64obj2 := obj2.(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) + } if float64obj1 > float64obj2 { return compareGreater, true } @@ -185,8 +281,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { } case reflect.String: { - stringobj1 := obj1.(string) - stringobj2 := obj2.(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) + } if stringobj1 > stringobj2 { return compareGreater, true } @@ -240,6 +342,24 @@ 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() diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index 49370eb..4dfd122 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -114,6 +114,24 @@ 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. // @@ -321,6 +339,54 @@ 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 { @@ -375,6 +441,17 @@ 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. // @@ -476,6 +553,15 @@ 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") @@ -572,6 +658,17 @@ 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") diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index 9db8894..25337a6 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -204,6 +204,42 @@ 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() @@ -631,6 +667,102 @@ 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 { @@ -739,6 +871,28 @@ 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. // @@ -941,6 +1095,24 @@ 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) @@ -1133,6 +1305,28 @@ 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") diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go new file mode 100644 index 0000000..1c3b471 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -0,0 +1,81 @@ +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) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index 914a10d..bcac440 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -172,8 +172,8 @@ func isTest(name, prefix string) bool { if len(name) == len(prefix) { // "Test" is ok return true } - rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) - return !unicode.IsLower(rune) + r, _ := utf8.DecodeRuneInString(name[len(prefix):]) + return !unicode.IsLower(r) } func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { @@ -1622,6 +1622,7 @@ var spewConfig = spew.ConfigState{ DisableCapacities: true, SortKeys: true, DisableMethods: true, + MaxDepth: 10, } type tHelper interface { @@ -1693,3 +1694,81 @@ 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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 70f0694..fa10b85 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,6 @@ # cloud.google.com/go v0.49.0 cloud.google.com/go/compute/metadata -# github.com/1Password/connect-sdk-go v1.0.1 +# github.com/1Password/connect-sdk-go v1.2.0 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.6.1 +# github.com/stretchr/testify v1.7.0 github.com/stretchr/testify/assert # github.com/uber/jaeger-client-go v2.25.0+incompatible github.com/uber/jaeger-client-go