Просмотр исходного кода

feat: add configuring tier for aws parameter store (#4262)

* feat: add configuring tier for parameter store

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* add tests and update the documentation

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* resolved the complexity issue

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Gergely Brautigam 1 год назад
Родитель
Сommit
9934cf5286

+ 5 - 4
design/010-pushsecret-metadata.md

@@ -34,13 +34,14 @@ spec:
 
 ```
 
-Here is a overview of current implementations of PushSecret metadata:
+Here is an overview of current implementations of PushSecret metadata:
 
 ```yaml
 # AWS Parameter Store
-# more to come in https://github.com/external-secrets/external-secrets/pull/3581
-parameterStoreType: "..."
-parameterStoreKeyID: "..."
+apiVersion: kubernetes.external-secrets.io/v1alpha1
+kind: PushSecretMetadata
+spec:
+  secretType: StringList
 ```
 
 ```yaml

+ 11 - 4
docs/provider/aws-parameter-store.md

@@ -125,19 +125,26 @@ The SetSecret method for the Parameter Store allows the user to set the value st
 
 #### Additional Metadata for PushSecret
 
-Optionally, it is possible to configure additional options for the parameter such as `Type` and encryption Key. To control this behaviour you can set the following provider's `metadata`:
+Optionally, it is possible to configure additional options for the parameter. These are as follows:
+- type
+- keyID
+- tier & policies
+
+To control this behaviour you can set the following provider's `metadata`:
 
 ```yaml
 {% include 'aws-pm-push-secret-with-metadata.yaml' %}
 ```
 
-`parameterStoreType` takes three options. `String`, `StringList`, and `SecureString`, where `String` is the _default_.
+- `secretType` takes three options. `String`, `StringList`, and `SecureString`, where `String` 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/ssm` is the _default_. This property is only used if `secretType` is set as `SecureString`.
+- 
+
 
-`parameterStoreKeyID` takes a KMS Key `$ID` or `$ARN` (in case a key source is created in another account) as a string, where `alias/aws/ssm` is the _default_. This property is only used if `parameterStoreType` is set as `SecureString`.
 
 #### Check successful secret sync
 
-To be able to check that the secret has been succesfully synced you can run the following command:
+To be able to check that the secret has been successfully synced you can run the following command:
 
 ```bash
 kubectl get pushsecret pushsecret-example

+ 27 - 2
docs/snippets/aws-pm-push-secret-with-metadata.yaml

@@ -17,5 +17,30 @@ spec:
         remoteRef:
           remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)
       metadata:
-        parameterStoreType: "SecureString"
-        parameterStoreKeyID: "bb123123-b2b0-4f60-ac3a-44a13f0e6b6c"
+        apiVersion: kubernetes.external-secrets.io/v1alpha1
+        kind: PushSecretMetadata
+        spec:
+          secretType: SecureString
+          kmsKeyID: bb123123-b2b0-4f60-ac3a-44a13f0e6b6c
+          tier:
+            type: Advanced # default is Standard
+            policies:
+              - type: "Expiration"
+                version: "1.0"
+                attributes:
+                  timestamp: "2024-12-02T21:34:33.000Z"
+              - type: "ExpirationNotification"
+                version: "1.0"
+                attributes:
+                  before: "2"
+                  unit: "Days"
+              - type: "ExpirationNotification"
+                version: "1.0"
+                attributes:
+                  before: "30"
+                  unit: "Days"
+              - type: "NoChangeNotification"
+                version: "1.0"
+                attributes:
+                  after: "30"
+                  unit: "Days"

+ 54 - 21
pkg/provider/aws/parameterstore/parameterstore.go

@@ -29,6 +29,7 @@ import (
 	"github.com/aws/aws-sdk-go/service/ssm"
 	"github.com/tidwall/gjson"
 	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/utils/ptr"
 	ctrl "sigs.k8s.io/controller-runtime"
 
@@ -38,15 +39,21 @@ 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"
 )
 
-// Declares metadata information for pushing secrets to AWS Parameter Store.
-const (
-	pushSecretType  = "parameterStoreType"
-	storeTypeString = "String"
-	storeKeyID      = "parameterStoreKeyID"
-	pushSecretKeyID = "keyID"
-)
+// Tier defines policy details for PushSecret.
+type Tier struct {
+	Type     string                `json:"type"`
+	Policies *apiextensionsv1.JSON `json:"policies"`
+}
+
+// PushSecretMetadataSpec defines the spec for the metadata for PushSecret.
+type PushSecretMetadataSpec struct {
+	SecretType string `json:"secretType,omitempty"`
+	KMSKeyID   string `json:"kmsKeyID,omitempty"`
+	Tier       Tier   `json:"tier,omitempty"`
+}
 
 // https://github.com/external-secrets/external-secrets/issues/644
 var (
@@ -154,18 +161,9 @@ func (pm *ParameterStore) PushSecret(ctx context.Context, secret *corev1.Secret,
 		err   error
 	)
 
-	parameterTypeFormat, err := utils.FetchValueFromMetadata(pushSecretType, data.GetMetadata(), storeTypeString)
-	if err != nil {
-		return fmt.Errorf("failed to parse metadata: %w", err)
-	}
-
-	parameterKeyIDFormat, err := utils.FetchValueFromMetadata(storeKeyID, data.GetMetadata(), pushSecretKeyID)
+	meta, err := pm.constructMetadataWithDefaults(data.GetMetadata())
 	if err != nil {
-		return fmt.Errorf("failed to parse metadata: %w", err)
-	}
-
-	if parameterKeyIDFormat == "keyID" || parameterKeyIDFormat == "" {
-		parameterKeyIDFormat = "alias/aws/ssm"
+		return err
 	}
 
 	key := data.GetSecretKey()
@@ -183,12 +181,17 @@ func (pm *ParameterStore) PushSecret(ctx context.Context, secret *corev1.Secret,
 	secretRequest := ssm.PutParameterInput{
 		Name:      ptr.To(pm.prefix + data.GetRemoteKey()),
 		Value:     ptr.To(string(value)),
-		Type:      ptr.To(parameterTypeFormat),
+		Type:      ptr.To(meta.Spec.SecretType),
 		Overwrite: ptr.To(true),
 	}
 
-	if parameterTypeFormat == "SecureString" {
-		secretRequest.KeyId = &parameterKeyIDFormat
+	if meta.Spec.SecretType == "SecureString" {
+		secretRequest.KeyId = &meta.Spec.KMSKeyID
+	}
+
+	if meta.Spec.Tier.Type == "Advanced" {
+		secretRequest.Tier = ptr.To(meta.Spec.Tier.Type)
+		secretRequest.Policies = ptr.To(string(meta.Spec.Tier.Policies.Raw))
 	}
 
 	secretValue := ssm.GetParameterInput{
@@ -553,3 +556,33 @@ func (pm *ParameterStore) Validate() (esv1beta1.ValidationResult, error) {
 	}
 	return esv1beta1.ValidationResultReady, nil
 }
+
+func (pm *ParameterStore) 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.Tier.Type == "" {
+		meta.Spec.Tier.Type = "Standard"
+	}
+
+	if meta.Spec.SecretType == "" {
+		meta.Spec.SecretType = "String"
+	}
+
+	if meta.Spec.KMSKeyID == "" {
+		meta.Spec.KMSKeyID = "alias/aws/ssm"
+	}
+
+	return meta, nil
+}

+ 75 - 15
pkg/provider/aws/parameterstore/parameterstore_test.go

@@ -440,12 +440,14 @@ func TestPushSecret(t *testing.T) {
 			args: args{
 				store: makeValidParameterStore().Spec.Provider.AWS,
 				metadata: &apiextensionsv1.JSON{
-					Raw: []byte(`
-					{
-						"parameterStoreType": "SecureString", 
-						"parameterStoreKeyID": "arn:aws:kms:sa-east-1:00000000000:key/bb123123-b2b0-4f60-ac3a-44a13f0e6b6c"
-					}
-					`),
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"secretType": "SecureString",
+							"kmsKeyID": "arn:aws:kms:sa-east-1:00000000000:key/bb123123-b2b0-4f60-ac3a-44a13f0e6b6c"
+						}
+					}`),
 				},
 				client: fakeps.Client{
 					PutParameterWithContextFn:        fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
@@ -463,7 +465,13 @@ func TestPushSecret(t *testing.T) {
 			args: args{
 				store: makeValidParameterStore().Spec.Provider.AWS,
 				metadata: &apiextensionsv1.JSON{
-					Raw: []byte(`{"parameterStoreType": "StringList", "parameterStoreKeyID": "alias/aws/ssm"}`),
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"secretType": "StringList"
+						}
+					}`),
 				},
 				client: fakeps.Client{
 					PutParameterWithContextFn:        fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
@@ -491,7 +499,7 @@ func TestPushSecret(t *testing.T) {
 				},
 			},
 			want: want{
-				err: errors.New("failed to parse metadata: failed to parse JSON raw data: invalid character 'f' looking for beginning of object key string"),
+				err: errors.New(`failed to parse metadata: failed to parse kubernetes.external-secrets.io/v1alpha1 PushSecretMetadata: error unmarshaling JSON: while decoding JSON: json: unknown field "fakeMetadataKey"`),
 			},
 		},
 		"GetRemoteSecretWithoutDecryption": {
@@ -499,12 +507,64 @@ func TestPushSecret(t *testing.T) {
 			args: args{
 				store: makeValidParameterStore().Spec.Provider.AWS,
 				metadata: &apiextensionsv1.JSON{
-					Raw: []byte(`
-					{
-						"parameterStoreType": "SecureString",
-						"parameterStoreKeyID": "arn:aws:kms:sa-east-1:00000000000:key/bb123123-b2b0-4f60-ac3a-44a13f0e6b6c"
-					}
-					`),
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"secretType": "SecureString",
+							"kmsKeyID": "arn:aws:kms:sa-east-1:00000000000:key/bb123123-b2b0-4f60-ac3a-44a13f0e6b6c"
+						}
+					}`),
+				},
+				client: fakeps.Client{
+					PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
+					GetParameterWithContextFn: fakeps.NewGetParameterWithContextFn(&ssm.GetParameterOutput{
+						Parameter: &ssm.Parameter{
+							Type:  aws.String("SecureString"),
+							Value: aws.String("sensitive"),
+						},
+					}, nil),
+					DescribeParametersWithContextFn:  fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
+					ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: errors.New("unable to compare 'sensitive' result, ensure to request a decrypted value"),
+			},
+		},
+		"SecretWithAdvancedTier": {
+			reason: "test if we can provide advanced tier policies",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"secretType": "SecureString",
+							"kmsKeyID": "arn:aws:kms:sa-east-1:00000000000:key/bb123123-b2b0-4f60-ac3a-44a13f0e6b6c",
+							"tier": {
+								"type": "Advanced",
+								"policies": [
+										{
+												"type": "Expiration",
+												"version": "1.0",
+												"attributes": {
+														"timestamp": "2024-12-02T21:34:33.000Z"
+												}
+										},
+										{
+												"type": "ExpirationNotification",
+												"version": "1.0",
+												"attributes": {
+														"before": "2",
+														"unit": "Days"
+												}
+										}
+								]
+							}
+						}
+					}`),
 				},
 				client: fakeps.Client{
 					PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
@@ -543,7 +603,7 @@ func TestPushSecret(t *testing.T) {
 			// if errors are the same type but their contents do not match
 			if err != nil && tc.want.err != nil {
 				if !strings.Contains(err.Error(), tc.want.err.Error()) {
-					t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
+					t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %s", name, tc.reason, tc.want.err, err)
 				}
 			}
 		})