Repository: external-secrets/external-secrets (go)

Issue:
Add `SecureString` for `PushString` to AWS Parameter Store
**Is your feature request related to a problem? Please describe.**
I'm baffled as to why this is not a customizable flag in the `PushSecret.spec` to set the parameter type for AWS SSM Parameter Store.
This is the offending line:

https://github.com/external-secrets/external-secrets/blob/e32bc394258b9ed26eb3a9c364e80843e9c08906/pkg/provider/aws/parameterstore/parameterstore.go#L141

This should obviously be something the client should be able to customize, with the default of `String` if you really want it. But, nevertheless, it shouldn't be a hard-coded value.

**Describe the solution you'd like**
I'd like to be able to modify the value to `SecureString` please. On the same topic, you should also be able to set the Parameter Tier of either `Standard` or `Advanced`. [reference](https://pkg.go.dev/github.com/aws/aws-sdk-go/service/ssm#PutParameterInput)

**Describe alternatives you've considered**
The alternative is to fork the repo and modify it to have the default **hard-coded** value of `SecureString` (cause I'm not a Go guy by any means :grimacing:).

**Additional context**
nada


Pull request description:
feat: add support to set Type for AWS parameter store
## Problem Statement

Currently, we don't have the option to set which parameter type to use for the AWS parameter store, and as a result, all secrets are stored remotely as plain text `String`. This PR adds the possibility to work with the following types `String`, `SecureString`, and `StringList` through `PushSecret`'s metadata.

## Related Issue

Fixes #3422

## Checklist

- [x] I have read the [contribution guidelines](https://external-secrets.io/latest/contributing/process/#submitting-a-pull-request)
- [X] All commits are signed with `git commit --signoff`
- [X] My changes have reasonable test coverage
- [X] All tests pass with `make test`
- [X] I ensured my PR is ready for review with `make reviewable`


Patch:
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index a9ee31f7d..c0ac8c9e7 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -5,14 +5,15 @@ describes governance guidelines and maintainer responsibilities.
 
 ## Maintainers
 
-| Maintainer | GitHub ID | Affiliation |
-| --------------- | --------- | ----------- |
-| RealName Here | [knelasevero](https://github.com/knelasevero) | [Company](https://www.github.com/Company/) |
+| Maintainer | GitHub ID                                       | Affiliation |
+| --------------- |-------------------------------------------------| ----------- |
+| RealName Here | [knelasevero](https://github.com/knelasevero)   | [Company](https://www.github.com/Company/) |
 | RealName Here | [gusfcarvalho](https://github.com/gusfcarvalho) | [Company](https://www.github.com/Company/) |
-| RealName Here | [moolen](https://github.com/moolen) | [Company](https://www.github.com/Company/) |
-| RealName Here | [sebagomez](https://github.com/sebagomez) | [Company](https://www.github.com/Company/) |
+| RealName Here | [moolen](https://github.com/moolen)             | [Company](https://www.github.com/Company/) |
+| RealName Here | [sebagomez](https://github.com/sebagomez)       | [Company](https://www.github.com/Company/) |
 | RealName Here | [rodrmartinez](https://github.com/rodrmartinez) | [Company](https://www.github.com/Company/) |
-| RealName Here | [IdanAdar](https://github.com/IdanAdar) | [Company](https://www.github.com/Company/) |
+| RealName Here | [IdanAdar](https://github.com/IdanAdar)         | [Company](https://www.github.com/Company/) |
+| RealName Here | [Skarlso](https://github.com/Skarlso)           | [Company](https://www.github.com/Company/) |
 
 
 ## External Secrets Operator Core Contributors & Stakeholders
diff --git a/docs/provider/aws-parameter-store.md b/docs/provider/aws-parameter-store.md
index cf7be0b0b..aeff67a16 100644
--- a/docs/provider/aws-parameter-store.md
+++ b/docs/provider/aws-parameter-store.md
@@ -101,6 +101,18 @@ The SetSecret method for the Parameter Store allows the user to set the value st
 {% include "full-pushsecret.yaml" %}
 ```
 
+#### 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`:
+
+```yaml
+{% include 'aws-pm-push-secret-with-metadata.yaml' %}
+```
+
+`parameterStoreType` takes three options. `String`, `StringList`, and `SecureString`, where `String` is the _default_.
+
+`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:
diff --git a/docs/snippets/aws-pm-push-secret-with-metadata.yaml b/docs/snippets/aws-pm-push-secret-with-metadata.yaml
new file mode 100644
index 000000000..b4999c3d2
--- /dev/null
+++ b/docs/snippets/aws-pm-push-secret-with-metadata.yaml
@@ -0,0 +1,21 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: pushsecret-example # Customisable
+  namespace: default # Same of the SecretStores
+spec:
+  deletionPolicy: Delete # the provider' secret will be deleted if the PushSecret is deleted
+  refreshInterval: 10s # Refresh interval for which push secret will reconcile
+  secretStoreRefs: # A list of secret stores to push secrets to
+    - name: aws-parameterstore
+      kind: SecretStore
+  selector:
+    secret:
+      name: pokedex-credentials # Source Kubernetes secret to be pushed
+  data:
+    - match:
+        remoteRef:
+          remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)
+      metadata:
+        parameterStoreType: "SecureString"
+        parameterStoreKeyID: "bb123123-b2b0-4f60-ac3a-44a13f0e6b6c"
diff --git a/pkg/provider/aws/parameterstore/parameterstore.go b/pkg/provider/aws/parameterstore/parameterstore.go
index d6b6f0be9..5f0d2cbf9 100644
--- a/pkg/provider/aws/parameterstore/parameterstore.go
+++ b/pkg/provider/aws/parameterstore/parameterstore.go
@@ -39,6 +39,16 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
+// Declares metadata information for pushing secrets to AWS Parameter Store.
+const (
+	PushSecretType                 = "parameterStoreType"
+	ParameterStoreTypeString       = "String"
+	ParameterStoreTypeStringList   = "StringList"
+	ParameterStoreTypeSecureString = "SecureString"
+	ParameterStoreKeyID            = "parameterStoreKeyID"
+	PushSecretKeyID                = "keyID"
+)
+
 // https://github.com/external-secrets/external-secrets/issues/644
 var (
 	_               esv1beta1.SecretsClient = &ParameterStore{}
@@ -138,13 +148,27 @@ func (pm *ParameterStore) SecretExists(_ context.Context, _ esv1beta1.PushSecret
 }
 
 func (pm *ParameterStore) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
-	parameterType := "String"
-	overwrite := true
-
 	var (
 		value []byte
 		err   error
 	)
+
+	parameterTypeFormat, err := utils.FetchValueFromMetadata(PushSecretType, data.GetMetadata(), ParameterStoreTypeString)
+	if err != nil {
+		return fmt.Errorf("failed to parse metadata: %w", err)
+	}
+
+	parameterKeyIDFormat, err := utils.FetchValueFromMetadata(ParameterStoreKeyID, data.GetMetadata(), PushSecretKeyID)
+	if err != nil {
+		return fmt.Errorf("failed to parse metadata: %w", err)
+	}
+
+	if parameterKeyIDFormat == "keyID" || parameterKeyIDFormat == "" {
+		parameterKeyIDFormat = "alias/aws/ssm"
+	}
+
+	overwrite := true
+
 	key := data.GetSecretKey()
 
 	if key == "" {
@@ -162,10 +186,14 @@ func (pm *ParameterStore) PushSecret(ctx context.Context, secret *corev1.Secret,
 	secretRequest := ssm.PutParameterInput{
 		Name:      &secretName,
 		Value:     &stringValue,
-		Type:      &parameterType,
+		Type:      &parameterTypeFormat,
 		Overwrite: &overwrite,
 	}
 
+	if parameterTypeFormat == "SecureString" {
+		secretRequest.KeyId = &parameterKeyIDFormat
+	}
+
 	secretValue := ssm.GetParameterInput{
 		Name: &secretName,
 	}


Test patch:
diff --git a/pkg/provider/aws/parameterstore/parameterstore_test.go b/pkg/provider/aws/parameterstore/parameterstore_test.go
index 2dc81bcaa..966ac8de9 100644
--- a/pkg/provider/aws/parameterstore/parameterstore_test.go
+++ b/pkg/provider/aws/parameterstore/parameterstore_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/aws/aws-sdk-go/service/ssm"
 	"github.com/google/go-cmp/cmp"
 	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@@ -306,8 +307,9 @@ func TestPushSecret(t *testing.T) {
 	}
 
 	type args struct {
-		store  *esv1beta1.AWSProvider
-		client fakeps.Client
+		store    *esv1beta1.AWSProvider
+		metadata *apiextensionsv1.JSON
+		client   fakeps.Client
 	}
 
 	type want struct {
@@ -424,11 +426,73 @@ func TestPushSecret(t *testing.T) {
 				err: nil,
 			},
 		},
+		"SetSecretWithValidMetadata": {
+			reason: "test push secret with valid parameterStoreType metadata",
+			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"
+					}
+					`),
+				},
+				client: fakeps.Client{
+					PutParameterWithContextFn:        fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
+					GetParameterWithContextFn:        fakeps.NewGetParameterWithContextFn(sameGetParameterOutput, nil),
+					DescribeParametersWithContextFn:  fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
+					ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithValidMetadataListString": {
+			reason: "test push secret with valid parameterStoreType metadata and unused parameterStoreKeyID",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{"parameterStoreType": "StringList", "parameterStoreKeyID": "alias/aws/ssm"}`),
+				},
+				client: fakeps.Client{
+					PutParameterWithContextFn:        fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
+					GetParameterWithContextFn:        fakeps.NewGetParameterWithContextFn(sameGetParameterOutput, nil),
+					DescribeParametersWithContextFn:  fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
+					ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithInvalidMetadata": {
+			reason: "test push secret with invalid metadata structure",
+			args: args{
+				store: makeValidParameterStore().Spec.Provider.AWS,
+				metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{ fakeMetadataKey: "" }`),
+				},
+				client: fakeps.Client{
+					PutParameterWithContextFn:        fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
+					GetParameterWithContextFn:        fakeps.NewGetParameterWithContextFn(sameGetParameterOutput, nil),
+					DescribeParametersWithContextFn:  fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
+					ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
+				},
+			},
+			want: want{
+				err: fmt.Errorf("failed to parse metadata: failed to parse JSON raw data: invalid character 'f' looking for beginning of object key string"),
+			},
+		},
 	}
 
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
 			psd := fake.PushSecretData{SecretKey: fakeSecretKey, RemoteKey: "fake-key"}
+			if tc.args.metadata != nil {
+				psd.Metadata = tc.args.metadata
+			}
 			ps := ParameterStore{
 				client: &tc.args.client,
 			}