Browse Source

feat(aws): support for aws tags (#4538)

* feat(aws): support tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(aws): support for aws tags

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Ivan Ka 1 year ago
parent
commit
87afb6702b

+ 10 - 3
docs/provider/aws-secrets-manager.md

@@ -117,15 +117,22 @@ Additional settings can be set at the `SecretStore` level to control the behavio
 
 #### Additional Metadata for PushSecret
 
-It's possible to configure AWS Secrets Manager to either push secrets in `binary` format or as plain `string`.
+Optionally, it is possible to configure additional options for the parameter. These are as follows:
+- kmsKeyID
+- secretPushFormat
+- description
+- tags
 
-To control this behaviour set the following provider metadata:
+To control this behavior set the following provider metadata:
 
 ```yaml
 {% include 'aws-sm-push-secret-with-metadata.yaml' %}
 ```
 
-`secretPushFormat` takes two options. `binary` and `string`, where `binary` is the _default_.
+- `secretPushFormat` takes two options. `binary` and `string`, where `binary` is the _default_.
+- `kmsKeyID` takes a KMS Key `$ID` or `$ARN` (in case a key source is created in another account) as a string, where `alias/aws/secretsmanager` is the _default_.
+- `description` Description of the secret.
+- `tags` Key-value map of user-defined tags that are attached to the secret.
 
 ### JSON Secret Values
 

+ 9 - 1
docs/snippets/aws-sm-push-secret-with-metadata.yaml

@@ -18,4 +18,12 @@ spec:
         remoteRef:
           remoteKey: teamb-my-first-parameter-3 # Remote reference (where the secret is going to be pushed)
       metadata:
-        secretPushFormat: string
+        apiVersion: kubernetes.external-secrets.io/v1alpha1
+        kind: PushSecretMetadata
+        spec:
+          kmsKeyID: bb123123-b2b0-4f60-ac3a-44a13f0e6b6c # When not set, default to alias/aws/secretsmanager
+          secretPushFormat: string # When not set, default to binary
+          description: "secret 'managed-by:secret-manager' from 'secret-store:teamb-secret-store'"
+          tags:
+            secret-store: teamb-secret-store
+            refresh-interval: 1h

+ 74 - 11
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -21,6 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"math/big"
+	"slices"
 	"strings"
 
 	"github.com/aws/aws-sdk-go/aws"
@@ -32,6 +33,7 @@ import (
 	"github.com/tidwall/gjson"
 	"github.com/tidwall/sjson"
 	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	utilpointer "k8s.io/utils/ptr"
 	ctrl "sigs.k8s.io/controller-runtime"
 
@@ -41,8 +43,16 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/metrics"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
 	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/utils/metadata"
 )
 
+type PushSecretMetadataSpec struct {
+	Tags             map[string]string `json:"tags,omitempty"`
+	Description      string            `json:"description,omitempty"`
+	SecretPushFormat string            `json:"secretPushFormat,omitempty"`
+	KMSKeyID         string            `json:"kmsKeyId,omitempty"`
+}
+
 // Declares metadata information for pushing secrets to AWS Secret Store.
 const (
 	SecretPushFormatKey    = "secretPushFormat"
@@ -492,26 +502,41 @@ func (sm *SecretsManager) Capabilities() esv1beta1.SecretStoreCapabilities {
 }
 
 func (sm *SecretsManager) createSecretWithContext(ctx context.Context, secretName string, psd esv1beta1.PushSecretData, value []byte) error {
-	secretPushFormat, err := utils.FetchValueFromMetadata(SecretPushFormatKey, psd.GetMetadata(), SecretPushFormatBinary)
+	mdata, err := sm.constructMetadataWithDefaults(psd.GetMetadata())
 	if err != nil {
-		return fmt.Errorf("failed to parse metadata: %w", err)
+		return fmt.Errorf("failed to parse push secret metadata: %w", err)
 	}
 
-	input := &awssm.CreateSecretInput{
-		Name:         &secretName,
-		SecretBinary: value,
-		Tags: []*awssm.Tag{
-			{
-				Key:   utilpointer.To(managedBy),
-				Value: utilpointer.To(externalSecrets),
-			},
+	tags := []*awssm.Tag{
+		{
+			Key:   utilpointer.To(managedBy),
+			Value: utilpointer.To(externalSecrets),
 		},
+	}
+
+	for k, v := range mdata.Spec.Tags {
+		tags = append(tags, &awssm.Tag{
+			Key:   utilpointer.To(k),
+			Value: utilpointer.To(v),
+		})
+	}
+
+	input := &awssm.CreateSecretInput{
+		Name:               &secretName,
+		SecretBinary:       value,
+		Tags:               tags,
+		Description:        utilpointer.To(mdata.Spec.Description),
 		ClientRequestToken: utilpointer.To(initialVersion),
 	}
-	if secretPushFormat == SecretPushFormatString {
+	if mdata.Spec.SecretPushFormat == SecretPushFormatString {
 		input.SetSecretBinary(nil).SetSecretString(string(value))
 	}
 
+	err = input.Validate()
+	if err != nil {
+		return fmt.Errorf("failed to validate input: %w", err)
+	}
+
 	_, err = sm.client.CreateSecretWithContext(ctx, input)
 	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMCreateSecret, err)
 
@@ -648,3 +673,41 @@ func (sm *SecretsManager) constructSecretValue(ctx context.Context, key, ver str
 
 	return secretOut, err
 }
+
+func (sm *SecretsManager) constructMetadataWithDefaults(data *apiextensionsv1.JSON) (*metadata.PushSecretMetadata[PushSecretMetadataSpec], error) {
+	var (
+		meta *metadata.PushSecretMetadata[PushSecretMetadataSpec]
+		err  error
+	)
+
+	meta, err = metadata.ParseMetadataParameters[PushSecretMetadataSpec](data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse metadata: %w", err)
+	}
+
+	if meta == nil {
+		meta = &metadata.PushSecretMetadata[PushSecretMetadataSpec]{}
+	}
+
+	if meta.Spec.SecretPushFormat == "" {
+		meta.Spec.SecretPushFormat = SecretPushFormatBinary
+	} else if !slices.Contains([]string{SecretPushFormatBinary, SecretPushFormatString}, meta.Spec.SecretPushFormat) {
+		return nil, fmt.Errorf("invalid secret push format: %s", meta.Spec.SecretPushFormat)
+	}
+
+	if meta.Spec.Description == "" {
+		meta.Spec.Description = fmt.Sprintf("secret '%s:%s'", managedBy, externalSecrets)
+	}
+
+	if meta.Spec.KMSKeyID == "" {
+		meta.Spec.KMSKeyID = "alias/aws/secretsmanager"
+	}
+
+	if len(meta.Spec.Tags) > 0 {
+		if _, exists := meta.Spec.Tags[managedBy]; exists {
+			return nil, fmt.Errorf("error parsing tags in metadata: Cannot specify a '%s' tag", managedBy)
+		}
+	}
+
+	return meta, nil
+}

+ 58 - 2
pkg/provider/aws/secretsmanager/secretsmanager_test.go

@@ -415,6 +415,10 @@ func TestSetSecret(t *testing.T) {
 			Key:   &managedBy,
 			Value: &externalSecrets,
 		},
+		{
+			Key:   ptr.To("taname1"),
+			Value: ptr.To("tagvalue1"),
+		},
 	}
 
 	externalSecretsTagFaulty := []*awssm.Tag{
@@ -481,8 +485,13 @@ func TestSetSecret(t *testing.T) {
 	pushSecretDataWithoutProperty := fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: ""}
 	pushSecretDataWithoutSecretKey := fake.PushSecretData{RemoteKey: fakeKey, Property: ""}
 	pushSecretDataWithMetadata := fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "", Metadata: &apiextensionsv1.JSON{
-		Raw: []byte(`{"secretPushFormat": "string"}`),
-	}}
+		Raw: []byte(`{
+					"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+					"kind": "PushSecretMetadata",
+					"spec": {
+						"secretPushFormat": "string"
+					}
+				}`)}}
 	pushSecretDataWithProperty := fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "other-fake-property"}
 
 	type args struct {
@@ -547,6 +556,53 @@ func TestSetSecret(t *testing.T) {
 				err: nil,
 			},
 		},
+		"SetSecretSucceedsWithExistingSecretAndKMSKeyAndDescription": {
+			reason: "a secret can be pushed to aws secrets manager when it already exists",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(secretValueOutput, &getSecretCorrectErr),
+					CreateSecretWithContextFn:   fakesm.NewCreateSecretWithContextFn(secretOutput, nil),
+					PutSecretValueWithContextFn: fakesm.NewPutSecretValueWithContextFn(putSecretOutput, nil),
+					DescribeSecretWithContextFn: fakesm.NewDescribeSecretWithContextFn(tagSecretOutput, nil),
+				},
+				pushSecretData: fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "", Metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+							"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+							"kind": "PushSecretMetadata",
+							"spec": {
+								"kmsKeyID": "bb123123-b2b0-4f60-ac3a-44a13f0e6b6c",
+								"description": "this is a description"
+							}
+						}`)}},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretSucceedsWithExistingSecretAndAdditionalTags": {
+			reason: "a secret can be pushed to aws secrets manager when it already exists",
+			args: args{
+				store: makeValidSecretStore().Spec.Provider.AWS,
+				client: fakesm.Client{
+					GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(secretValueOutput, nil),
+					CreateSecretWithContextFn:   fakesm.NewCreateSecretWithContextFn(secretOutput, nil),
+					PutSecretValueWithContextFn: fakesm.NewPutSecretValueWithContextFn(putSecretOutput, nil),
+					DescribeSecretWithContextFn: fakesm.NewDescribeSecretWithContextFn(tagSecretOutput, nil),
+				},
+				pushSecretData: fake.PushSecretData{SecretKey: secretKey, RemoteKey: fakeKey, Property: "", Metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+							"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+							"kind": "PushSecretMetadata",
+							"spec": {
+								"tags": {"tagname12": "tagvalue1"}
+							}
+						}`)}},
+			},
+			want: want{
+				err: nil,
+			},
+		},
 		"SetSecretSucceedsWithNewSecret": {
 			reason: "a secret can be pushed to aws secrets manager if it doesn't already exist",
 			args: args{