Explorar o código

:sparkles: Support MetadataPolicy=Fetch for GCP Secrets Manager (#2111)

* Support MetadataPolicy=Fetch for GCP Secrets Manager

Signed-off-by: shuheiktgw <s-kitagawa@mercari.com>

* Use '.' instead of '/' to split metadata

Signed-off-by: shuheiktgw <s-kitagawa@mercari.com>

* Support annotations/labels

Signed-off-by: shuheiktgw <s-kitagawa@mercari.com>

---------

Signed-off-by: shuheiktgw <s-kitagawa@mercari.com>
Shuhei Kitagawa %!s(int64=3) %!d(string=hai) anos
pai
achega
07f237e071

+ 78 - 0
pkg/provider/gcp/secretmanager/client.go

@@ -336,6 +336,10 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 		return nil, fmt.Errorf(errUninitalizedGCPProvider)
 	}
 
+	if ref.MetadataPolicy == esv1beta1.ExternalSecretMetadataPolicyFetch {
+		return c.getSecretMetadata(ctx, ref)
+	}
+
 	version := ref.Version
 	if version == "" {
 		version = defaultVersion
@@ -378,6 +382,80 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 	return []byte(val.String()), nil
 }
 
+func (c *Client) getSecretMetadata(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	secret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
+		Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, ref.Key),
+	})
+
+	err = parseError(err)
+	if err != nil {
+		return nil, fmt.Errorf(errClientGetSecretAccess, err)
+	}
+
+	const (
+		annotations = "annotations"
+		labels      = "labels"
+	)
+
+	extractMetadataKey := func(s string, p string) string {
+		prefix := p + "."
+		if !strings.HasPrefix(s, prefix) {
+			return ""
+		}
+		return strings.TrimPrefix(s, prefix)
+	}
+
+	if annotation := extractMetadataKey(ref.Property, annotations); annotation != "" {
+		v, ok := secret.GetAnnotations()[annotation]
+		if !ok {
+			return nil, fmt.Errorf("annotation with key %s does not exist in secret %s", annotation, ref.Key)
+		}
+
+		return []byte(v), nil
+	}
+
+	if label := extractMetadataKey(ref.Property, labels); label != "" {
+		v, ok := secret.GetLabels()[label]
+		if !ok {
+			return nil, fmt.Errorf("label with key %s does not exist in secret %s", label, ref.Key)
+		}
+
+		return []byte(v), nil
+	}
+
+	if ref.Property == annotations {
+		j, err := json.Marshal(secret.GetAnnotations())
+		if err != nil {
+			return nil, fmt.Errorf("faild marshaling annotations into json: %w", err)
+		}
+
+		return j, nil
+	}
+
+	if ref.Property == labels {
+		j, err := json.Marshal(secret.GetLabels())
+		if err != nil {
+			return nil, fmt.Errorf("faild marshaling labels into json: %w", err)
+		}
+
+		return j, nil
+	}
+
+	if ref.Property != "" {
+		return nil, fmt.Errorf("invalid property %s: metadata property should start with either %s or %s", ref.Property, annotations, labels)
+	}
+
+	j, err := json.Marshal(map[string]map[string]string{
+		"annotations": secret.GetAnnotations(),
+		"labels":      secret.GetLabels(),
+	})
+	if err != nil {
+		return nil, fmt.Errorf("faild marshaling metadata map into json: %w", err)
+	}
+
+	return j, nil
+}
+
 // GetSecretMap returns multiple k/v pairs from the provider.
 func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	if c.smClient == nil || c.store.ProjectID == "" {

+ 202 - 0
pkg/provider/gcp/secretmanager/client_test.go

@@ -193,6 +193,207 @@ func TestSecretManagerGetSecret(t *testing.T) {
 	}
 }
 
+func TestGetSecret_MetadataPolicyFetch(t *testing.T) {
+	tests := []struct {
+		name                string
+		ref                 esv1beta1.ExternalSecretDataRemoteRef
+		getSecretMockReturn fakesm.GetSecretMockReturn
+		expectedSecret      string
+		expectedErr         string
+	}{
+		{
+			name: "annotation is specified",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:            "bar",
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Property:       "annotations.managed-by",
+			},
+			getSecretMockReturn: fakesm.GetSecretMockReturn{
+				Secret: &secretmanagerpb.Secret{
+					Name: "projects/foo/secret/bar",
+					Annotations: map[string]string{
+						"managed-by": "external-secrets",
+					},
+				},
+				Err: nil,
+			},
+			expectedSecret: "external-secrets",
+		},
+		{
+			name: "label is specified",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:            "bar",
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Property:       "labels.managed-by",
+			},
+			getSecretMockReturn: fakesm.GetSecretMockReturn{
+				Secret: &secretmanagerpb.Secret{
+					Name: "projects/foo/secret/bar",
+					Labels: map[string]string{
+						"managed-by": "external-secrets",
+					},
+				},
+				Err: nil,
+			},
+			expectedSecret: "external-secrets",
+		},
+		{
+			name: "annotations is specified",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:            "bar",
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Property:       "annotations",
+			},
+			getSecretMockReturn: fakesm.GetSecretMockReturn{
+				Secret: &secretmanagerpb.Secret{
+					Name: "projects/foo/secret/bar",
+					Annotations: map[string]string{
+						"annotationKey1": "annotationValue1",
+						"annotationKey2": "annotationValue2",
+					},
+					Labels: map[string]string{
+						"labelKey1": "labelValue1",
+						"labelKey2": "labelValue2",
+					},
+				},
+				Err: nil,
+			},
+			expectedSecret: `{"annotationKey1":"annotationValue1","annotationKey2":"annotationValue2"}`,
+		},
+		{
+			name: "labels is specified",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:            "bar",
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Property:       "labels",
+			},
+			getSecretMockReturn: fakesm.GetSecretMockReturn{
+				Secret: &secretmanagerpb.Secret{
+					Name: "projects/foo/secret/bar",
+					Annotations: map[string]string{
+						"annotationKey1": "annotationValue1",
+						"annotationKey2": "annotationValue2",
+					},
+					Labels: map[string]string{
+						"labelKey1": "labelValue1",
+						"labelKey2": "labelValue2",
+					},
+				},
+				Err: nil,
+			},
+			expectedSecret: `{"labelKey1":"labelValue1","labelKey2":"labelValue2"}`,
+		},
+		{
+			name: "no property is specified",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:            "bar",
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+			},
+			getSecretMockReturn: fakesm.GetSecretMockReturn{
+				Secret: &secretmanagerpb.Secret{
+					Name: "projects/foo/secret/bar",
+					Labels: map[string]string{
+						"label-key": "label-value",
+					},
+					Annotations: map[string]string{
+						"annotation-key": "annotation-value",
+					},
+				},
+				Err: nil,
+			},
+			expectedSecret: `{"annotations":{"annotation-key":"annotation-value"},"labels":{"label-key":"label-value"}}`,
+		},
+		{
+			name: "annotation does not exist",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:            "bar",
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Property:       "annotations.unknown",
+			},
+			getSecretMockReturn: fakesm.GetSecretMockReturn{
+				Secret: &secretmanagerpb.Secret{
+					Name: "projects/foo/secret/bar",
+					Annotations: map[string]string{
+						"managed-by": "external-secrets",
+					},
+				},
+				Err: nil,
+			},
+			expectedErr: "annotation with key unknown does not exist in secret bar",
+		},
+		{
+			name: "label does not exist",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:            "bar",
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Property:       "labels.unknown",
+			},
+			getSecretMockReturn: fakesm.GetSecretMockReturn{
+				Secret: &secretmanagerpb.Secret{
+					Name: "projects/foo/secret/bar",
+					Labels: map[string]string{
+						"managed-by": "external-secrets",
+					},
+				},
+				Err: nil,
+			},
+			expectedErr: "label with key unknown does not exist in secret bar",
+		},
+		{
+			name: "invalid property",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:            "bar",
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Property:       "invalid.managed-by",
+			},
+			getSecretMockReturn: fakesm.GetSecretMockReturn{
+				Secret: &secretmanagerpb.Secret{
+					Name: "projects/foo/secret/bar",
+					Labels: map[string]string{
+						"managed-by": "external-secrets",
+					},
+				},
+				Err: nil,
+			},
+			expectedErr: "invalid property invalid.managed-by",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			smClient := fakesm.MockSMClient{}
+			smClient.NewGetSecretFn(tc.getSecretMockReturn)
+
+			client := Client{
+				smClient: &smClient,
+				store: &esv1beta1.GCPSMProvider{
+					ProjectID: "foo",
+				},
+			}
+			got, err := client.GetSecret(context.TODO(), tc.ref)
+			if tc.expectedErr != "" {
+				if err == nil {
+					t.Fatalf("expected to receive an error but got nit")
+				}
+
+				if !ErrorContains(err, tc.expectedErr) {
+					t.Fatalf("unexpected error: %s, expected: '%s'", err.Error(), tc.expectedErr)
+				}
+
+				return
+			}
+
+			if err != nil {
+				t.Fatalf("unexpected error: %s", err)
+			}
+
+			if gotStr := string(got); gotStr != tc.expectedSecret {
+				t.Fatalf("unexpected secret: expected %s, got %s", tc.expectedSecret, gotStr)
+			}
+		})
+	}
+}
+
 type fakeRef struct {
 	key string
 }
@@ -309,6 +510,7 @@ func TestDeleteSecret(t *testing.T) {
 		})
 	}
 }
+
 func TestSetSecret(t *testing.T) {
 	ref := fakeRef{key: "/baz"}