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

feat(aws): add parameterstore v2 provider e2e coverage

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 2 месяцев назад
Родитель
Сommit
741aee6c14
44 измененных файлов с 3882 добавлено и 183 удалено
  1. 3 0
      apis/provider/aws/v2alpha1/groupversion_info.go
  2. 93 0
      apis/provider/aws/v2alpha1/parameterstore_types.go
  3. 118 0
      apis/provider/aws/v2alpha1/zz_generated.deepcopy.go
  4. 1 0
      config/crds/bases/kustomization.yaml
  5. 294 0
      config/crds/bases/provider.external-secrets.io_parameterstores.yaml
  6. 284 0
      deploy/crds/bundle.yaml
  7. 2 2
      e2e/Makefile
  8. 39 6
      e2e/framework/addon/chart.go
  9. 20 0
      e2e/framework/addon/chart_test.go
  10. 13 0
      e2e/framework/addon/eso_v2_mutators.go
  11. 10 0
      e2e/framework/addon/eso_v2_mutators_test.go
  12. 5 1
      e2e/framework/framework.go
  13. 7 0
      e2e/framework/testcase.go
  14. 60 0
      e2e/framework/util/util.go
  15. 126 0
      e2e/framework/util/util_test.go
  16. 5 1
      e2e/framework/v2/helpers.go
  17. 145 0
      e2e/suites/provider/cases/aws/common_test.go
  18. 109 0
      e2e/suites/provider/cases/aws/parameterstore/clusterprovider_v2.go
  19. 343 0
      e2e/suites/provider/cases/aws/parameterstore/provider_support_v2.go
  20. 56 0
      e2e/suites/provider/cases/aws/parameterstore/provider_support_v2_test.go
  21. 120 0
      e2e/suites/provider/cases/aws/parameterstore/provider_v2.go
  22. 72 0
      e2e/suites/provider/cases/aws/parameterstore/provider_v2_test.go
  23. 152 0
      e2e/suites/provider/cases/aws/parameterstore/push_v2.go
  24. 109 0
      e2e/suites/provider/cases/aws/secretsmanager/clusterprovider_v2.go
  25. 15 63
      e2e/suites/provider/cases/aws/secretsmanager/provider.go
  26. 484 0
      e2e/suites/provider/cases/aws/secretsmanager/provider_support.go
  27. 210 0
      e2e/suites/provider/cases/aws/secretsmanager/provider_support_test.go
  28. 181 0
      e2e/suites/provider/cases/aws/secretsmanager/provider_v2.go
  29. 161 0
      e2e/suites/provider/cases/aws/secretsmanager/push_v2.go
  30. 88 0
      e2e/suites/provider/cases/aws/secretsmanager/secretsmanager_v2_managed.go
  31. 131 0
      e2e/suites/provider/cases/aws/v2_support.go
  32. 46 42
      e2e/suites/provider/cases/common/fake_provider.go
  33. 45 0
      e2e/suites/provider/cases/common/fake_provider_test.go
  34. 29 0
      e2e/suites/provider/cases/common/provider_runtime_test.go
  35. 29 25
      e2e/suites/provider/cases/common/push_secret.go
  36. 82 0
      e2e/suites/provider/cases/fake/provider_v2_test.go
  37. 7 0
      e2e/suites/provider/cases/fake/regressions.go
  38. 53 26
      providers/v2/aws/config.go
  39. 120 0
      providers/v2/aws/config_test.go
  40. 7 4
      providers/v2/aws/main.go
  41. 6 1
      providers/v2/aws/provider.yaml
  42. 1 4
      providers/v2/fake/main.go
  43. 0 4
      providers/v2/hack/templates/main.go.tmpl
  44. 1 4
      providers/v2/kubernetes/main.go

+ 3 - 0
apis/provider/aws/v2alpha1/groupversion_info.go

@@ -28,6 +28,9 @@ var (
 	// SecretsManagerKind is the kind name used for SecretsManager resources.
 	// SecretsManagerKind is the kind name used for SecretsManager resources.
 	SecretsManagerKind = "SecretsManager"
 	SecretsManagerKind = "SecretsManager"
 
 
+	// ParameterStoreKind is the kind name used for ParameterStore resources.
+	ParameterStoreKind = "ParameterStore"
+
 	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
 	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
 	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
 	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
 
 

+ 93 - 0
apis/provider/aws/v2alpha1/parameterstore_types.go

@@ -0,0 +1,93 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+// ParameterStoreSpec defines the desired state of ParameterStore.
+type ParameterStoreSpec struct {
+	// Auth defines the information necessary to authenticate against AWS
+	// if not set aws sdk will infer credentials from your environment
+	// see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials
+	// +optional
+	Auth v1.AWSAuth `json:"auth,omitempty"`
+
+	// Role is a Role ARN which the provider will assume
+	// +optional
+	Role string `json:"role,omitempty"`
+
+	// AWS Region to be used for the provider
+	Region string `json:"region"`
+
+	// AdditionalRoles is a chained list of Role ARNs which the provider will sequentially assume before assuming the Role
+	// +optional
+	AdditionalRoles []string `json:"additionalRoles,omitempty"`
+
+	// AWS External ID set on assumed IAM roles
+	ExternalID string `json:"externalID,omitempty"`
+
+	// AWS STS assume role session tags
+	// +optional
+	SessionTags []*v1.Tag `json:"sessionTags,omitempty"`
+
+	// AWS STS assume role transitive session tags. Required when multiple rules are used with the provider
+	// +optional
+	TransitiveTagKeys []string `json:"transitiveTagKeys,omitempty"`
+
+	// Prefix adds a prefix to all retrieved values.
+	// +optional
+	Prefix string `json:"prefix,omitempty"`
+}
+
+// ParameterStoreStatus defines the observed state of ParameterStore.
+type ParameterStoreStatus struct {
+	// Conditions represent the latest available observations of the resource's state.
+	// +optional
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:scope=Namespaced,categories={externalsecrets},shortName=ssm
+// +kubebuilder:printcolumn:name="Region",type=string,JSONPath=`.spec.region`
+// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
+
+// ParameterStore is the Schema for AWS Parameter Store provider configuration.
+type ParameterStore struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   ParameterStoreSpec   `json:"spec,omitempty"`
+	Status ParameterStoreStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// ParameterStoreList contains a list of ParameterStore.
+type ParameterStoreList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []ParameterStore `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&ParameterStore{}, &ParameterStoreList{})
+}

+ 118 - 0
apis/provider/aws/v2alpha1/zz_generated.deepcopy.go

@@ -26,6 +26,124 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 )
 
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ParameterStore) DeepCopyInto(out *ParameterStore) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParameterStore.
+func (in *ParameterStore) DeepCopy() *ParameterStore {
+	if in == nil {
+		return nil
+	}
+	out := new(ParameterStore)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ParameterStore) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ParameterStoreList) DeepCopyInto(out *ParameterStoreList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]ParameterStore, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParameterStoreList.
+func (in *ParameterStoreList) DeepCopy() *ParameterStoreList {
+	if in == nil {
+		return nil
+	}
+	out := new(ParameterStoreList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ParameterStoreList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ParameterStoreSpec) DeepCopyInto(out *ParameterStoreSpec) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+	if in.AdditionalRoles != nil {
+		in, out := &in.AdditionalRoles, &out.AdditionalRoles
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.SessionTags != nil {
+		in, out := &in.SessionTags, &out.SessionTags
+		*out = make([]*v1.Tag, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(v1.Tag)
+				**out = **in
+			}
+		}
+	}
+	if in.TransitiveTagKeys != nil {
+		in, out := &in.TransitiveTagKeys, &out.TransitiveTagKeys
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParameterStoreSpec.
+func (in *ParameterStoreSpec) DeepCopy() *ParameterStoreSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ParameterStoreSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ParameterStoreStatus) DeepCopyInto(out *ParameterStoreStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]metav1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParameterStoreStatus.
+func (in *ParameterStoreStatus) DeepCopy() *ParameterStoreStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(ParameterStoreStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SecretsManager) DeepCopyInto(out *SecretsManager) {
 func (in *SecretsManager) DeepCopyInto(out *SecretsManager) {
 	*out = *in
 	*out = *in

+ 1 - 0
config/crds/bases/kustomization.yaml

@@ -29,4 +29,5 @@ resources:
   - generators.external-secrets.io_webhooks.yaml
   - generators.external-secrets.io_webhooks.yaml
   - provider.external-secrets.io_fakes.yaml
   - provider.external-secrets.io_fakes.yaml
   - provider.external-secrets.io_kubernetes.yaml
   - provider.external-secrets.io_kubernetes.yaml
+  - provider.external-secrets.io_parameterstores.yaml
   - provider.external-secrets.io_secretsmanagers.yaml
   - provider.external-secrets.io_secretsmanagers.yaml

+ 294 - 0
config/crds/bases/provider.external-secrets.io_parameterstores.yaml

@@ -0,0 +1,294 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.19.0
+  name: parameterstores.provider.external-secrets.io
+spec:
+  group: provider.external-secrets.io
+  names:
+    categories:
+    - externalsecrets
+    kind: ParameterStore
+    listKind: ParameterStoreList
+    plural: parameterstores
+    shortNames:
+    - ssm
+    singular: parameterstore
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.region
+      name: Region
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v2alpha1
+    schema:
+      openAPIV3Schema:
+        description: ParameterStore is the Schema for AWS Parameter Store provider
+          configuration.
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: ParameterStoreSpec defines the desired state of ParameterStore.
+            properties:
+              additionalRoles:
+                description: AdditionalRoles is a chained list of Role ARNs which
+                  the provider will sequentially assume before assuming the Role
+                items:
+                  type: string
+                type: array
+              auth:
+                description: |-
+                  Auth defines the information necessary to authenticate against AWS
+                  if not set aws sdk will infer credentials from your environment
+                  see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials
+                properties:
+                  jwt:
+                    description: AWSJWTAuth stores reference to Authenticate against
+                      AWS using service account tokens.
+                    properties:
+                      serviceAccountRef:
+                        description: ServiceAccountSelector is a reference to a ServiceAccount
+                          resource.
+                        properties:
+                          audiences:
+                            description: |-
+                              Audience specifies the `aud` claim for the service account token
+                              If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                              then this audiences will be appended to the list
+                            items:
+                              type: string
+                            type: array
+                          name:
+                            description: The name of the ServiceAccount resource being
+                              referred to.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              Namespace of the resource being referred to.
+                              Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                        required:
+                        - name
+                        type: object
+                    type: object
+                  secretRef:
+                    description: |-
+                      AWSAuthSecretRef holds secret references for AWS credentials
+                      both AccessKeyID and SecretAccessKey must be defined in order to properly authenticate.
+                    properties:
+                      accessKeyIDSecretRef:
+                        description: The AccessKeyID is used for authentication
+                        properties:
+                          key:
+                            description: |-
+                              A key in the referenced Secret.
+                              Some instances of this field may be defaulted, in others it may be required.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the Secret resource being referred
+                              to.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              The namespace of the Secret resource being referred to.
+                              Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                        type: object
+                      secretAccessKeySecretRef:
+                        description: The SecretAccessKey is used for authentication
+                        properties:
+                          key:
+                            description: |-
+                              A key in the referenced Secret.
+                              Some instances of this field may be defaulted, in others it may be required.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the Secret resource being referred
+                              to.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              The namespace of the Secret resource being referred to.
+                              Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                        type: object
+                      sessionTokenSecretRef:
+                        description: |-
+                          The SessionToken used for authentication
+                          This must be defined if AccessKeyID and SecretAccessKey are temporary credentials
+                          see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html
+                        properties:
+                          key:
+                            description: |-
+                              A key in the referenced Secret.
+                              Some instances of this field may be defaulted, in others it may be required.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the Secret resource being referred
+                              to.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              The namespace of the Secret resource being referred to.
+                              Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                        type: object
+                    type: object
+                type: object
+              externalID:
+                description: AWS External ID set on assumed IAM roles
+                type: string
+              prefix:
+                description: Prefix adds a prefix to all retrieved values.
+                type: string
+              region:
+                description: AWS Region to be used for the provider
+                type: string
+              role:
+                description: Role is a Role ARN which the provider will assume
+                type: string
+              sessionTags:
+                description: AWS STS assume role session tags
+                items:
+                  description: |-
+                    Tag is a key-value pair that can be attached to an AWS resource.
+                    see: https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html
+                  properties:
+                    key:
+                      type: string
+                    value:
+                      type: string
+                  required:
+                  - key
+                  - value
+                  type: object
+                type: array
+              transitiveTagKeys:
+                description: AWS STS assume role transitive session tags. Required
+                  when multiple rules are used with the provider
+                items:
+                  type: string
+                type: array
+            required:
+            - region
+            type: object
+          status:
+            description: ParameterStoreStatus defines the observed state of ParameterStore.
+            properties:
+              conditions:
+                description: Conditions represent the latest available observations
+                  of the resource's state.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

+ 284 - 0
deploy/crds/bundle.yaml

@@ -30446,6 +30446,290 @@ spec:
 ---
 ---
 apiVersion: apiextensions.k8s.io/v1
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.19.0
+  name: parameterstores.provider.external-secrets.io
+spec:
+  group: provider.external-secrets.io
+  names:
+    categories:
+      - externalsecrets
+    kind: ParameterStore
+    listKind: ParameterStoreList
+    plural: parameterstores
+    shortNames:
+      - ssm
+    singular: parameterstore
+  scope: Namespaced
+  versions:
+    - additionalPrinterColumns:
+        - jsonPath: .spec.region
+          name: Region
+          type: string
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
+      name: v2alpha1
+      schema:
+        openAPIV3Schema:
+          description: ParameterStore is the Schema for AWS Parameter Store provider configuration.
+          properties:
+            apiVersion:
+              description: |-
+                APIVersion defines the versioned schema of this representation of an object.
+                Servers should convert recognized schemas to the latest internal value, and
+                may reject unrecognized values.
+                More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+              type: string
+            kind:
+              description: |-
+                Kind is a string value representing the REST resource this object represents.
+                Servers may infer this from the endpoint the client submits requests to.
+                Cannot be updated.
+                In CamelCase.
+                More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+              type: string
+            metadata:
+              type: object
+            spec:
+              description: ParameterStoreSpec defines the desired state of ParameterStore.
+              properties:
+                additionalRoles:
+                  description: AdditionalRoles is a chained list of Role ARNs which the provider will sequentially assume before assuming the Role
+                  items:
+                    type: string
+                  type: array
+                auth:
+                  description: |-
+                    Auth defines the information necessary to authenticate against AWS
+                    if not set aws sdk will infer credentials from your environment
+                    see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials
+                  properties:
+                    jwt:
+                      description: AWSJWTAuth stores reference to Authenticate against AWS using service account tokens.
+                      properties:
+                        serviceAccountRef:
+                          description: ServiceAccountSelector is a reference to a ServiceAccount resource.
+                          properties:
+                            audiences:
+                              description: |-
+                                Audience specifies the `aud` claim for the service account token
+                                If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                then this audiences will be appended to the list
+                              items:
+                                type: string
+                              type: array
+                            name:
+                              description: The name of the ServiceAccount resource being referred to.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                Namespace of the resource being referred to.
+                                Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                          required:
+                            - name
+                          type: object
+                      type: object
+                    secretRef:
+                      description: |-
+                        AWSAuthSecretRef holds secret references for AWS credentials
+                        both AccessKeyID and SecretAccessKey must be defined in order to properly authenticate.
+                      properties:
+                        accessKeyIDSecretRef:
+                          description: The AccessKeyID is used for authentication
+                          properties:
+                            key:
+                              description: |-
+                                A key in the referenced Secret.
+                                Some instances of this field may be defaulted, in others it may be required.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the Secret resource being referred to.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                The namespace of the Secret resource being referred to.
+                                Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                          type: object
+                        secretAccessKeySecretRef:
+                          description: The SecretAccessKey is used for authentication
+                          properties:
+                            key:
+                              description: |-
+                                A key in the referenced Secret.
+                                Some instances of this field may be defaulted, in others it may be required.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the Secret resource being referred to.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                The namespace of the Secret resource being referred to.
+                                Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                          type: object
+                        sessionTokenSecretRef:
+                          description: |-
+                            The SessionToken used for authentication
+                            This must be defined if AccessKeyID and SecretAccessKey are temporary credentials
+                            see: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html
+                          properties:
+                            key:
+                              description: |-
+                                A key in the referenced Secret.
+                                Some instances of this field may be defaulted, in others it may be required.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the Secret resource being referred to.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                The namespace of the Secret resource being referred to.
+                                Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                          type: object
+                      type: object
+                  type: object
+                externalID:
+                  description: AWS External ID set on assumed IAM roles
+                  type: string
+                prefix:
+                  description: Prefix adds a prefix to all retrieved values.
+                  type: string
+                region:
+                  description: AWS Region to be used for the provider
+                  type: string
+                role:
+                  description: Role is a Role ARN which the provider will assume
+                  type: string
+                sessionTags:
+                  description: AWS STS assume role session tags
+                  items:
+                    description: |-
+                      Tag is a key-value pair that can be attached to an AWS resource.
+                      see: https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html
+                    properties:
+                      key:
+                        type: string
+                      value:
+                        type: string
+                    required:
+                      - key
+                      - value
+                    type: object
+                  type: array
+                transitiveTagKeys:
+                  description: AWS STS assume role transitive session tags. Required when multiple rules are used with the provider
+                  items:
+                    type: string
+                  type: array
+              required:
+                - region
+              type: object
+            status:
+              description: ParameterStoreStatus defines the observed state of ParameterStore.
+              properties:
+                conditions:
+                  description: Conditions represent the latest available observations of the resource's state.
+                  items:
+                    description: Condition contains details for one aspect of the current state of this API Resource.
+                    properties:
+                      lastTransitionTime:
+                        description: |-
+                          lastTransitionTime is the last time the condition transitioned from one status to another.
+                          This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                        format: date-time
+                        type: string
+                      message:
+                        description: |-
+                          message is a human readable message indicating details about the transition.
+                          This may be an empty string.
+                        maxLength: 32768
+                        type: string
+                      observedGeneration:
+                        description: |-
+                          observedGeneration represents the .metadata.generation that the condition was set based upon.
+                          For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                          with respect to the current state of the instance.
+                        format: int64
+                        minimum: 0
+                        type: integer
+                      reason:
+                        description: |-
+                          reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                          Producers of specific condition types may define expected values and meanings for this field,
+                          and whether the values are considered a guaranteed API.
+                          The value should be a CamelCase string.
+                          This field may not be empty.
+                        maxLength: 1024
+                        minLength: 1
+                        pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                        type: string
+                      status:
+                        description: status of the condition, one of True, False, Unknown.
+                        enum:
+                          - "True"
+                          - "False"
+                          - Unknown
+                        type: string
+                      type:
+                        description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                        maxLength: 316
+                        pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                        type: string
+                    required:
+                      - lastTransitionTime
+                      - message
+                      - reason
+                      - status
+                      - type
+                    type: object
+                  type: array
+              type: object
+          type: object
+      served: true
+      storage: true
+      subresources:
+        status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
 metadata:
   annotations:
   annotations:
     controller-gen.kubebuilder.io/version: v0.19.0
     controller-gen.kubebuilder.io/version: v0.19.0

+ 2 - 2
e2e/Makefile

@@ -108,7 +108,7 @@ test.managed: e2e-image ## Run e2e tests against current kube context
 
 
 
 
 e2e-bin: install-ginkgo
 e2e-bin: install-ginkgo
-	   CGO_ENABLED=0 ginkgo build ./suites/...
+	   GOWORK=off CGO_ENABLED=0 ginkgo build ./suites/...
 
 
 e2e-image: e2e-bin
 e2e-image: e2e-bin
 	-rm -rf ./k8s/deploy
 	-rm -rf ./k8s/deploy
@@ -121,7 +121,7 @@ e2e-image: e2e-bin
 GINKGO_VERSION := $(shell grep 'github.com/onsi/ginkgo/v2' go.mod | awk '{print $$2}')
 GINKGO_VERSION := $(shell grep 'github.com/onsi/ginkgo/v2' go.mod | awk '{print $$2}')
 install-ginkgo:
 install-ginkgo:
 	   @echo "Installing ginkgo version $(GINKGO_VERSION) from go.mod"
 	   @echo "Installing ginkgo version $(GINKGO_VERSION) from go.mod"
-	   go install github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION)
+	   GOWORK=off go install github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION)
 
 
 help: ## displays this help message
 help: ## displays this help message
 	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_\/-]+:.*?## / {printf "\033[34m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | \
 	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_\/-]+:.*?## / {printf "\033[34m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | \

+ 39 - 6
e2e/framework/addon/chart.go

@@ -22,6 +22,7 @@ import (
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"path/filepath"
 	"path/filepath"
+	"strings"
 
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/ginkgo/v2"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -81,11 +82,21 @@ func (c *HelmChart) Install() error {
 	}
 	}
 
 
 	args := c.installArgs()
 	args := c.installArgs()
-	log.Logf("installing chart with args: %+q", args)
-	cmd := exec.Command("helm", args...)
-	output, err := cmd.CombinedOutput()
+	output, err := c.runInstall(args)
 	if err != nil {
 	if err != nil {
-		return fmt.Errorf("unable to run cmd: %w: %s", err, string(output))
+		if !isHelmReleaseNameInUseError(string(output)) {
+			return fmt.Errorf("unable to run cmd: %w: %s", err, string(output))
+		}
+
+		log.Logf("helm install detected stale release state for %q in namespace %q; attempting cleanup", c.ReleaseName, c.Namespace)
+		if cleanupErr := c.cleanupExistingRelease(); cleanupErr != nil {
+			return fmt.Errorf("unable to clean stale helm release %s/%s after install failure: %w", c.Namespace, c.ReleaseName, cleanupErr)
+		}
+
+		output, err = c.runInstall(args)
+		if err != nil {
+			return fmt.Errorf("unable to run cmd after stale release cleanup: %w: %s", err, string(output))
+		}
 	}
 	}
 
 
 	log.Logf("finished running chart install")
 	log.Logf("finished running chart install")
@@ -126,10 +137,32 @@ func (c *HelmChart) installArgs() []string {
 	return args
 	return args
 }
 }
 
 
+func (c *HelmChart) uninstallArgs() []string {
+	return []string{"uninstall", "--namespace", c.Namespace, c.ReleaseName, "--wait", "--ignore-not-found"}
+}
+
+func (c *HelmChart) runInstall(args []string) ([]byte, error) {
+	log.Logf("installing chart with args: %+q", args)
+	cmd := exec.Command("helm", args...)
+	return cmd.CombinedOutput()
+}
+
+func (c *HelmChart) cleanupExistingRelease() error {
+	cmd := exec.Command("helm", c.uninstallArgs()...)
+	output, err := cmd.CombinedOutput()
+	if err != nil && !strings.Contains(string(output), "release: not found") {
+		return fmt.Errorf("unable to uninstall stale helm release: %w: %s", err, string(output))
+	}
+	return nil
+}
+
+func isHelmReleaseNameInUseError(output string) bool {
+	return strings.Contains(output, "cannot re-use a name that is still in use")
+}
+
 // Uninstall removes the chart aswell as the repo.
 // Uninstall removes the chart aswell as the repo.
 func (c *HelmChart) Uninstall() error {
 func (c *HelmChart) Uninstall() error {
-	args := []string{"uninstall", "--namespace", c.Namespace, c.ReleaseName, "--wait"}
-	cmd := exec.Command("helm", args...)
+	cmd := exec.Command("helm", c.uninstallArgs()...)
 	output, err := cmd.CombinedOutput()
 	output, err := cmd.CombinedOutput()
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("unable to uninstall helm release: %w: %s", err, string(output))
 		return fmt.Errorf("unable to uninstall helm release: %w: %s", err, string(output))

+ 20 - 0
e2e/framework/addon/chart_test.go

@@ -63,6 +63,26 @@ func TestInstallArgsOmitDependencyUpdateWhenSkipped(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestUninstallArgsIncludeIgnoreNotFound(t *testing.T) {
+	args := (&HelmChart{
+		ReleaseName: "external-secrets",
+		Namespace:   "external-secrets-system",
+	}).uninstallArgs()
+
+	if !contains(args, "--ignore-not-found") {
+		t.Fatalf("expected uninstall args to include --ignore-not-found, got %v", args)
+	}
+}
+
+func TestIsHelmReleaseNameInUseError(t *testing.T) {
+	if !isHelmReleaseNameInUseError("Error: INSTALLATION FAILED: cannot re-use a name that is still in use") {
+		t.Fatal("expected stale release message to be detected")
+	}
+	if isHelmReleaseNameInUseError("release: not found") {
+		t.Fatal("did not expect unrelated helm output to be detected as stale release state")
+	}
+}
+
 func contains(values []string, want string) bool {
 func contains(values []string, want string) bool {
 	for _, value := range values {
 	for _, value := range values {
 		if value == want {
 		if value == want {

+ 13 - 0
e2e/framework/addon/eso_v2_mutators.go

@@ -58,6 +58,19 @@ func WithV2AWSProvider() MutationFunc {
 	}
 	}
 }
 }
 
 
+func WithV2ProviderServiceAccount(providerName, serviceAccountName string) MutationFunc {
+	return func(eso *ESO) {
+		index := findProviderIndex(eso.HelmChart, providerName)
+		if index < 0 {
+			panic("provider entry must exist before overriding service account")
+		}
+
+		prefix := "providers.list[" + strconv.Itoa(index) + "].serviceAccount"
+		setOrAppendVar(eso.HelmChart, StringTuple{Key: prefix + ".create", Value: "false"})
+		setOrAppendVar(eso.HelmChart, StringTuple{Key: prefix + ".name", Value: serviceAccountName})
+	}
+}
+
 func setOrAppendVar(chart *HelmChart, variable StringTuple) {
 func setOrAppendVar(chart *HelmChart, variable StringTuple) {
 	for i := range chart.Vars {
 	for i := range chart.Vars {
 		if chart.Vars[i].Key == variable.Key {
 		if chart.Vars[i].Key == variable.Key {

+ 10 - 0
e2e/framework/addon/eso_v2_mutators_test.go

@@ -194,6 +194,16 @@ func TestWithV2FakeProviderEnforcesRequiredFlags(t *testing.T) {
 	assertVarValue(t, eso.HelmChart, "providerDefaults.replicaCount", "8")
 	assertVarValue(t, eso.HelmChart, "providerDefaults.replicaCount", "8")
 }
 }
 
 
+func TestWithV2ProviderServiceAccountOverridesAWSInPlace(t *testing.T) {
+	t.Setenv("VERSION", "test-version")
+
+	eso := NewESO(WithV2AWSProvider())
+	WithV2ProviderServiceAccount("aws", "irsa-sa")(eso)
+
+	assertVarValue(t, eso.HelmChart, "providers.list[0].serviceAccount.create", "false")
+	assertVarValue(t, eso.HelmChart, "providers.list[0].serviceAccount.name", "irsa-sa")
+}
+
 func assertVarValue(t *testing.T, chart *HelmChart, key, wantValue string) {
 func assertVarValue(t *testing.T, chart *HelmChart, key, wantValue string) {
 	t.Helper()
 	t.Helper()
 
 

+ 5 - 1
e2e/framework/framework.go

@@ -85,6 +85,8 @@ func New(baseName string) *Framework {
 // BeforeEach creates a namespace.
 // BeforeEach creates a namespace.
 func (f *Framework) BeforeEach() {
 func (f *Framework) BeforeEach() {
 	var err error
 	var err error
+	err = util.CleanupTerminatingE2ENamespaces(GinkgoT().Context(), f.CRClient)
+	Expect(err).ToNot(HaveOccurred())
 	f.Namespace, err = util.CreateKubeNamespace(f.BaseName, f.KubeClientSet)
 	f.Namespace, err = util.CreateKubeNamespace(f.BaseName, f.KubeClientSet)
 	log.Logf("created test namespace %s", f.Namespace.Name)
 	log.Logf("created test namespace %s", f.Namespace.Name)
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
@@ -104,7 +106,9 @@ func (f *Framework) AfterEach() {
 	// reset addons to default once the run is done
 	// reset addons to default once the run is done
 	f.Addons = []addon.Addon{}
 	f.Addons = []addon.Addon{}
 	log.Logf("deleting test namespace %s", f.Namespace.Name)
 	log.Logf("deleting test namespace %s", f.Namespace.Name)
-	err := util.DeleteKubeNamespace(f.Namespace.Name, f.KubeClientSet)
+	err := util.ClearKnownNamespaceFinalizers(GinkgoT().Context(), f.CRClient, f.Namespace.Name)
+	Expect(err).NotTo(HaveOccurred())
+	err = util.DeleteKubeNamespace(f.Namespace.Name, f.KubeClientSet)
 	Expect(err).NotTo(HaveOccurred())
 	Expect(err).NotTo(HaveOccurred())
 }
 }
 
 

+ 7 - 0
e2e/framework/testcase.go

@@ -42,6 +42,7 @@ type TestCase struct {
 	Secrets                 map[string]SecretEntry
 	Secrets                 map[string]SecretEntry
 	ExpectedSecret          *v1.Secret
 	ExpectedSecret          *v1.Secret
 	Prepare                 func(*TestCase, SecretStoreProvider)
 	Prepare                 func(*TestCase, SecretStoreProvider)
+	Cleanup                 func()
 	ProviderOverride        SecretStoreProvider
 	ProviderOverride        SecretStoreProvider
 	AfterSync               func(SecretStoreProvider, *v1.Secret)
 	AfterSync               func(SecretStoreProvider, *v1.Secret)
 	VerifyPushSecretOutcome func(ps *esv1alpha1.PushSecret, pushClient esv1.SecretsClient)
 	VerifyPushSecretOutcome func(ps *esv1alpha1.PushSecret, pushClient esv1.SecretsClient)
@@ -69,6 +70,12 @@ func TableFuncWithExternalSecret(f *Framework, prov SecretStoreProvider) func(..
 			tweak(tc)
 			tweak(tc)
 		}
 		}
 
 
+		defer func() {
+			if tc.Cleanup != nil {
+				tc.Cleanup()
+			}
+		}()
+
 		prov = prepareTestCase(tc, prov)
 		prov = prepareTestCase(tc, prov)
 
 
 		// create secrets & defer delete
 		// create secrets & defer delete

+ 60 - 0
e2e/framework/util/util.go

@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"strings"
 	"time"
 	"time"
 
 
 	fluxhelm "github.com/fluxcd/helm-controller/api/v2"
 	fluxhelm "github.com/fluxcd/helm-controller/api/v2"
@@ -79,6 +80,8 @@ func init() {
 const (
 const (
 	// How often to poll for conditions.
 	// How often to poll for conditions.
 	Poll = 2 * time.Second
 	Poll = 2 * time.Second
+
+	e2eNamespacePrefix = "e2e-tests-"
 )
 )
 
 
 // CreateKubeNamespace creates a new Kubernetes Namespace for a test.
 // CreateKubeNamespace creates a new Kubernetes Namespace for a test.
@@ -97,6 +100,63 @@ func DeleteKubeNamespace(namespace string, kubeClientSet kubernetes.Interface) e
 	return kubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), namespace, metav1.DeleteOptions{})
 	return kubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), namespace, metav1.DeleteOptions{})
 }
 }
 
 
+func IsE2ETestNamespace(namespace string) bool {
+	return strings.HasPrefix(namespace, e2eNamespacePrefix)
+}
+
+func ClearKnownNamespaceFinalizers(ctx context.Context, c crclient.Client, namespace string) error {
+	var secretList v1.SecretList
+	if err := c.List(ctx, &secretList, crclient.InNamespace(namespace)); err != nil {
+		return err
+	}
+	for i := range secretList.Items {
+		if len(secretList.Items[i].Finalizers) == 0 {
+			continue
+		}
+		secret := secretList.Items[i].DeepCopy()
+		secret.Finalizers = nil
+		if err := c.Update(ctx, secret); err != nil {
+			return err
+		}
+	}
+
+	var pushSecretList esv1alpha1.PushSecretList
+	if err := c.List(ctx, &pushSecretList, crclient.InNamespace(namespace)); err != nil {
+		return err
+	}
+	for i := range pushSecretList.Items {
+		if len(pushSecretList.Items[i].Finalizers) == 0 {
+			continue
+		}
+		pushSecret := pushSecretList.Items[i].DeepCopy()
+		pushSecret.Finalizers = nil
+		if err := c.Update(ctx, pushSecret); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func CleanupTerminatingE2ENamespaces(ctx context.Context, c crclient.Client) error {
+	var namespaceList v1.NamespaceList
+	if err := c.List(ctx, &namespaceList); err != nil {
+		return err
+	}
+
+	for i := range namespaceList.Items {
+		namespace := namespaceList.Items[i]
+		if !IsE2ETestNamespace(namespace.Name) || namespace.DeletionTimestamp == nil {
+			continue
+		}
+		if err := ClearKnownNamespaceFinalizers(ctx, c, namespace.Name); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // WaitForKubeNamespaceNotExist will wait for the namespace with the given name
 // WaitForKubeNamespaceNotExist will wait for the namespace with the given name
 // to not exist for up to 2 minutes.
 // to not exist for up to 2 minutes.
 func WaitForKubeNamespaceNotExist(namespace string, kubeClientSet kubernetes.Interface) error {
 func WaitForKubeNamespaceNotExist(namespace string, kubeClientSet kubernetes.Interface) error {

+ 126 - 0
e2e/framework/util/util_test.go

@@ -0,0 +1,126 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package util
+
+import (
+	"context"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+func TestClearKnownNamespaceFinalizers(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+		&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:       "target-secret",
+				Namespace:  "e2e-tests-demo-12345",
+				Finalizers: []string{"example.com/finalizer"},
+			},
+		},
+		&esv1alpha1.PushSecret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:       "push-secret",
+				Namespace:  "e2e-tests-demo-12345",
+				Finalizers: []string{"pushsecret.externalsecrets.io/finalizer"},
+			},
+		},
+	).Build()
+
+	if err := ClearKnownNamespaceFinalizers(ctx, cl, "e2e-tests-demo-12345"); err != nil {
+		t.Fatalf("ClearKnownNamespaceFinalizers() error = %v", err)
+	}
+
+	var secret corev1.Secret
+	if err := cl.Get(ctx, client.ObjectKey{Name: "target-secret", Namespace: "e2e-tests-demo-12345"}, &secret); err != nil {
+		t.Fatalf("Get(secret) error = %v", err)
+	}
+	if len(secret.Finalizers) != 0 {
+		t.Fatalf("expected secret finalizers to be cleared, got %v", secret.Finalizers)
+	}
+
+	var pushSecret esv1alpha1.PushSecret
+	if err := cl.Get(ctx, client.ObjectKey{Name: "push-secret", Namespace: "e2e-tests-demo-12345"}, &pushSecret); err != nil {
+		t.Fatalf("Get(pushsecret) error = %v", err)
+	}
+	if len(pushSecret.Finalizers) != 0 {
+		t.Fatalf("expected pushsecret finalizers to be cleared, got %v", pushSecret.Finalizers)
+	}
+}
+
+func TestCleanupTerminatingE2ENamespaces(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	now := metav1.Now()
+	cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+		&corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:       "e2e-tests-demo-12345",
+				Finalizers: []string{"kubernetes"},
+				DeletionTimestamp: &now,
+			},
+		},
+		&corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "plain-namespace",
+			},
+		},
+		&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:       "target-secret",
+				Namespace:  "e2e-tests-demo-12345",
+				Finalizers: []string{"example.com/finalizer"},
+			},
+		},
+		&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:       "untouched-secret",
+				Namespace:  "plain-namespace",
+				Finalizers: []string{"example.com/finalizer"},
+			},
+		},
+	).Build()
+
+	if err := CleanupTerminatingE2ENamespaces(ctx, cl); err != nil {
+		t.Fatalf("CleanupTerminatingE2ENamespaces() error = %v", err)
+	}
+
+	var cleaned corev1.Secret
+	if err := cl.Get(ctx, client.ObjectKey{Name: "target-secret", Namespace: "e2e-tests-demo-12345"}, &cleaned); err != nil {
+		t.Fatalf("Get(cleaned) error = %v", err)
+	}
+	if len(cleaned.Finalizers) != 0 {
+		t.Fatalf("expected terminating e2e namespace secret finalizers to be cleared, got %v", cleaned.Finalizers)
+	}
+
+	var untouched corev1.Secret
+	if err := cl.Get(ctx, client.ObjectKey{Name: "untouched-secret", Namespace: "plain-namespace"}, &untouched); err != nil {
+		t.Fatalf("Get(untouched) error = %v", err)
+	}
+	if len(untouched.Finalizers) != 1 {
+		t.Fatalf("expected non-e2e namespace secret finalizers to remain, got %v", untouched.Finalizers)
+	}
+}

+ 5 - 1
e2e/framework/v2/helpers.go

@@ -43,7 +43,11 @@ const (
 )
 )
 
 
 func ProviderAddress(providerName string) string {
 func ProviderAddress(providerName string) string {
-	return fmt.Sprintf("provider-%s.%s.svc:8080", providerName, ProviderNamespace)
+	return ProviderAddressInNamespace(providerName, ProviderNamespace)
+}
+
+func ProviderAddressInNamespace(providerName, namespace string) string {
+	return fmt.Sprintf("provider-%s.%s.svc:8080", providerName, namespace)
 }
 }
 
 
 func GetClusterCABundle(f *framework.Framework, namespace string) []byte {
 func GetClusterCABundle(f *framework.Framework, namespace string) []byte {

+ 145 - 0
e2e/suites/provider/cases/aws/common_test.go

@@ -0,0 +1,145 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package common
+
+import (
+	"strings"
+	"testing"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestCredentialsSecretName(t *testing.T) {
+	t.Parallel()
+
+	if got := CredentialsSecretName("aws-config"); got != "aws-config-credentials" {
+		t.Fatalf("unexpected credentials secret name: %q", got)
+	}
+}
+
+func TestStaticCredentialsSecretDataPreservesSessionToken(t *testing.T) {
+	t.Parallel()
+
+	got := StaticCredentialsSecretData("kid", "sak", "st")
+	if got[StaticAccessKeyIDKey] != "kid" {
+		t.Fatalf("unexpected access key id: %q", got[StaticAccessKeyIDKey])
+	}
+	if got[StaticSecretAccessKeyKey] != "sak" {
+		t.Fatalf("unexpected secret access key: %q", got[StaticSecretAccessKeyKey])
+	}
+	if got[StaticSessionTokenKey] != "st" {
+		t.Fatalf("unexpected session token: %q", got[StaticSessionTokenKey])
+	}
+}
+
+func TestProviderConfigNamespaceForManifestScope(t *testing.T) {
+	t.Parallel()
+
+	if got := ProviderConfigNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns", "workload-ns"); got != "workload-ns" {
+		t.Fatalf("expected workload namespace, got %q", got)
+	}
+}
+
+func TestProviderConfigNamespaceForProviderScope(t *testing.T) {
+	t.Parallel()
+
+	if got := ProviderConfigNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns", "workload-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace, got %q", got)
+	}
+}
+
+func TestProviderReferenceNamespaceForManifestScope(t *testing.T) {
+	t.Parallel()
+
+	if got := ProviderReferenceNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns"); got != "" {
+		t.Fatalf("expected empty provider reference namespace, got %q", got)
+	}
+}
+
+func TestProviderReferenceNamespaceForProviderScope(t *testing.T) {
+	t.Parallel()
+
+	if got := ProviderReferenceNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace, got %q", got)
+	}
+}
+
+func TestNewV2ClusterProviderScenarioManifestScope(t *testing.T) {
+	t.Parallel()
+
+	called := false
+	got := NewV2ClusterProviderScenario("workload-ns", "case", esv1.AuthenticationScopeManifestNamespace, func(prefix string) string {
+		called = true
+		return prefix + "-provider"
+	})
+	if called {
+		t.Fatal("expected provider namespace factory to be unused for manifest scope")
+	}
+	if got.ConfigName != "case-config" {
+		t.Fatalf("unexpected config name: %q", got.ConfigName)
+	}
+	if got.ConfigNamespace != "workload-ns" {
+		t.Fatalf("unexpected config namespace: %q", got.ConfigNamespace)
+	}
+	if got.ProviderNamespace != "workload-ns" {
+		t.Fatalf("unexpected provider namespace: %q", got.ProviderNamespace)
+	}
+	if got.ProviderRefNamespace != "" {
+		t.Fatalf("expected empty provider reference namespace, got %q", got.ProviderRefNamespace)
+	}
+	if got.WorkloadNamespace != "workload-ns" {
+		t.Fatalf("unexpected workload namespace: %q", got.WorkloadNamespace)
+	}
+	if got.NamePrefix != "workload-ns-case" {
+		t.Fatalf("unexpected name prefix: %q", got.NamePrefix)
+	}
+}
+
+func TestNewV2ClusterProviderScenarioProviderScope(t *testing.T) {
+	t.Parallel()
+
+	var gotPrefix string
+	got := NewV2ClusterProviderScenario("workload-ns", "case", esv1.AuthenticationScopeProviderNamespace, func(prefix string) string {
+		gotPrefix = prefix
+		return "provider-ns"
+	})
+	if gotPrefix != "case-provider" {
+		t.Fatalf("unexpected provider namespace prefix: %q", gotPrefix)
+	}
+	if got.ConfigNamespace != "provider-ns" {
+		t.Fatalf("unexpected config namespace: %q", got.ConfigNamespace)
+	}
+	if got.ProviderNamespace != "provider-ns" {
+		t.Fatalf("unexpected provider namespace: %q", got.ProviderNamespace)
+	}
+	if got.ProviderRefNamespace != "provider-ns" {
+		t.Fatalf("unexpected provider reference namespace: %q", got.ProviderRefNamespace)
+	}
+}
+
+func TestPushSecretMetadataWithRemoteNamespace(t *testing.T) {
+	t.Parallel()
+
+	got := PushSecretMetadataWithRemoteNamespace("target-ns")
+	if got == nil {
+		t.Fatal("expected metadata payload")
+	}
+	raw := string(got.Raw)
+	if !strings.Contains(raw, `"remoteNamespace":"target-ns"`) {
+		t.Fatalf("expected remote namespace in metadata, got %q", raw)
+	}
+}

+ 109 - 0
e2e/suites/provider/cases/aws/parameterstore/clusterprovider_v2.go

@@ -0,0 +1,109 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+)
+
+var _ = Describe("[aws] v2 cluster provider", Label("aws", "parameterstore", "v2", "cluster-provider"), func() {
+	f := framework.New("eso-aws-ps-v2-clusterprovider")
+	prov := NewProviderV2(f)
+	harness := newAWSClusterProviderExternalSecretHarness(f, prov)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("cluster provider external secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.ClusterProviderManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderDeniedByConditions(f, harness)),
+	)
+})
+
+type awsClusterProviderScenario struct {
+	common               awscommon.V2ClusterProviderScenario
+	access               awsV2AccessConfig
+	authScope            esv1.AuthenticationScope
+	backend              *parameterStoreBackend
+	f                    *framework.Framework
+}
+
+func newAWSClusterProviderScenario(f *framework.Framework, prefix string, authScope esv1.AuthenticationScope, access awsV2AccessConfig, backend *parameterStoreBackend) *awsClusterProviderScenario {
+	shared := awscommon.NewV2ClusterProviderScenario(f.Namespace.Name, prefix, authScope, func(prefix string) string {
+		return common.CreateProviderCaseNamespace(f, prefix, defaultV2PollInterval)
+	})
+	s := &awsClusterProviderScenario{
+		common:    shared,
+		access:    access,
+		authScope: authScope,
+		backend:   backend,
+		f:         f,
+	}
+	createParameterStoreV2Config(s.f, s.common.ConfigNamespace, s.common.ConfigName, s.access)
+	return s
+}
+
+func (s *awsClusterProviderScenario) createClusterProvider(conditions []esv1.ClusterSecretStoreCondition) string {
+	clusterProviderName := s.common.ClusterProviderName()
+	frameworkv2.CreateClusterProviderConnection(
+		s.f,
+		clusterProviderName,
+		frameworkv2.ProviderAddress("aws"),
+		awsProviderAPIVersion,
+		awsv2alpha1.ParameterStoreKind,
+		s.common.ConfigName,
+		s.common.ProviderRefNamespace,
+		s.common.AuthScope,
+		conditions,
+	)
+	return clusterProviderName
+}
+
+func (s *awsClusterProviderScenario) CreateSecret(key string, val framework.SecretEntry) {
+	s.backend.CreateSecret(key, val)
+}
+
+func (s *awsClusterProviderScenario) DeleteSecret(key string) {
+	s.backend.DeleteSecret(key)
+}
+
+func newAWSClusterProviderExternalSecretHarness(f *framework.Framework, prov *ProviderV2) common.ClusterProviderExternalSecretHarness {
+	return common.ClusterProviderExternalSecretHarness{
+		Prepare: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderExternalSecretRuntime {
+			s := newAWSClusterProviderScenario(f, cfg.Name, cfg.AuthScope, prov.access, prov.backend)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderExternalSecretRuntime{
+				ClusterProviderName: clusterProviderName,
+				Provider:            s,
+			}
+		},
+	}
+}

+ 343 - 0
e2e/suites/provider/cases/aws/parameterstore/provider_support_v2.go

@@ -0,0 +1,343 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	"github.com/aws/aws-sdk-go-v2/service/ssm"
+	ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	"github.com/external-secrets/external-secrets-e2e/framework/log"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+)
+
+const (
+	awsProviderAPIVersion = "provider.external-secrets.io/v2alpha1"
+	defaultV2WaitTimeout  = 60 * time.Second
+	defaultV2PollInterval = 2 * time.Second
+)
+
+type awsV2AccessConfig struct {
+	KID    string
+	SAK    string
+	ST     string
+	Region string
+}
+
+type parameterStoreBackend struct {
+	access    awsV2AccessConfig
+	client    *ssm.Client
+	clientErr error
+	clientOnce sync.Once
+}
+
+func loadAWSV2AccessConfigFromEnv() awsV2AccessConfig {
+	return awsV2AccessConfig{
+		KID:    os.Getenv("AWS_ACCESS_KEY_ID"),
+		SAK:    os.Getenv("AWS_SECRET_ACCESS_KEY"),
+		ST:     os.Getenv("AWS_SESSION_TOKEN"),
+		Region: os.Getenv("AWS_REGION"),
+	}
+}
+
+func (c awsV2AccessConfig) missingStaticCredentials() []string {
+	var missing []string
+	if c.KID == "" {
+		missing = append(missing, "AWS_ACCESS_KEY_ID")
+	}
+	if c.SAK == "" {
+		missing = append(missing, "AWS_SECRET_ACCESS_KEY")
+	}
+	if c.Region == "" {
+		missing = append(missing, "AWS_REGION")
+	}
+	return missing
+}
+
+func skipIfAWSV2StaticCredentialsMissing(access awsV2AccessConfig) {
+	if missing := access.missingStaticCredentials(); len(missing) > 0 {
+		Skip("missing AWS e2e credentials: " + strings.Join(missing, ", "))
+	}
+}
+
+func staticAWSV2Auth(secretName string) esv1.AWSAuth {
+	return esv1.AWSAuth{
+		SecretRef: &esv1.AWSAuthSecretRef{
+			AccessKeyID: esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticAccessKeyIDKey,
+			},
+			SecretAccessKey: esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticSecretAccessKeyKey,
+			},
+			SessionToken: &esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticSessionTokenKey,
+			},
+		},
+	}
+}
+
+func newStaticCredentialsSecret(namespace, name string, access awsV2AccessConfig) *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		StringData: awscommon.StaticCredentialsSecretData(access.KID, access.SAK, access.ST),
+	}
+}
+
+func createStaticCredentialsSecret(f *framework.Framework, namespace, name string, access awsV2AccessConfig) {
+	Expect(f.CRClient.Create(GinkgoT().Context(), newStaticCredentialsSecret(namespace, name, access))).To(Succeed())
+}
+
+func newParameterStoreV2Config(namespace, name string, access awsV2AccessConfig) *awsv2alpha1.ParameterStore {
+	return &awsv2alpha1.ParameterStore{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: awsv2alpha1.GroupVersion.String(),
+			Kind:       awsv2alpha1.ParameterStoreKind,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: awsv2alpha1.ParameterStoreSpec{
+			Region: access.Region,
+			Auth:   staticAWSV2Auth(awscommon.CredentialsSecretName(name)),
+		},
+	}
+}
+
+func createParameterStoreV2Config(f *framework.Framework, namespace, name string, access awsV2AccessConfig) *awsv2alpha1.ParameterStore {
+	createStaticCredentialsSecret(f, namespace, awscommon.CredentialsSecretName(name), access)
+	cfg := newParameterStoreV2Config(namespace, name, access)
+	Expect(f.CRClient.Create(GinkgoT().Context(), cfg)).To(Succeed())
+	return cfg
+}
+
+func loadParameterStoreAWSConfig(access awsV2AccessConfig) (aws.Config, error) {
+	loadOptions := []func(*config.LoadOptions) error{
+		config.WithRegion(access.Region),
+	}
+	if access.KID != "" || access.SAK != "" || access.ST != "" {
+		loadOptions = append(loadOptions, config.WithCredentialsProvider(
+			credentials.NewStaticCredentialsProvider(access.KID, access.SAK, access.ST),
+		))
+	}
+	return config.LoadDefaultConfig(context.Background(), loadOptions...)
+}
+
+func newParameterStoreBackend(access awsV2AccessConfig) *parameterStoreBackend {
+	return &parameterStoreBackend{access: access}
+}
+
+func (b *parameterStoreBackend) ensureClient() {
+	b.clientOnce.Do(func() {
+		cfg, err := loadParameterStoreAWSConfig(b.access)
+		if err != nil {
+			b.clientErr = err
+			return
+		}
+		b.client = ssm.NewFromConfig(cfg)
+	})
+
+	Expect(b.clientErr).ToNot(HaveOccurred())
+	Expect(b.client).NotTo(BeNil())
+}
+
+func (b *parameterStoreBackend) CreateSecret(key string, val framework.SecretEntry) {
+	b.ensureClient()
+
+	psTags := make([]ssmtypes.Tag, 0, len(val.Tags))
+	for tagKey, tagValue := range val.Tags {
+		psTags = append(psTags, ssmtypes.Tag{
+			Key:   aws.String(tagKey),
+			Value: aws.String(tagValue),
+		})
+	}
+
+	overwrite := len(psTags) == 0
+	_, err := b.client.PutParameter(GinkgoT().Context(), &ssm.PutParameterInput{
+		Name:      aws.String(key),
+		Value:     aws.String(val.Value),
+		DataType:  aws.String("text"),
+		Type:      ssmtypes.ParameterTypeString,
+		Overwrite: aws.Bool(overwrite),
+		Tags:      psTags,
+	})
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (b *parameterStoreBackend) DeleteSecret(key string) {
+	b.ensureClient()
+
+	_, err := b.client.DeleteParameter(GinkgoT().Context(), &ssm.DeleteParameterInput{
+		Name: aws.String(key),
+	})
+	var parameterNotFound *ssmtypes.ParameterNotFound
+	var resourceNotFound *ssmtypes.ResourceNotFoundException
+	if errors.As(err, &parameterNotFound) || errors.As(err, &resourceNotFound) {
+		return
+	}
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (b *parameterStoreBackend) WaitForSecretValue(name, expectedValue string) {
+	b.ensureClient()
+
+	Eventually(func(g Gomega) {
+		out, err := b.client.GetParameter(GinkgoT().Context(), &ssm.GetParameterInput{
+			Name:           aws.String(name),
+			WithDecryption: aws.Bool(true),
+		})
+		g.Expect(err).NotTo(HaveOccurred())
+		g.Expect(out.Parameter).NotTo(BeNil())
+		g.Expect(aws.ToString(out.Parameter.Value)).To(Equal(expectedValue))
+	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+}
+
+func (b *parameterStoreBackend) ExpectSecretAbsent(name string) {
+	b.ensureClient()
+
+	Eventually(func() bool {
+		_, err := b.client.GetParameter(GinkgoT().Context(), &ssm.GetParameterInput{
+			Name:           aws.String(name),
+			WithDecryption: aws.Bool(true),
+		})
+		return parameterStoreReadErrorIndicatesAbsence(err)
+	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(BeTrue(), fmt.Sprintf("expected AWS parameter %q to be absent", name))
+}
+
+func parameterStoreReadErrorIndicatesAbsence(err error) bool {
+	if err == nil {
+		return false
+	}
+	var parameterNotFound *ssmtypes.ParameterNotFound
+	var resourceNotFound *ssmtypes.ResourceNotFoundException
+	return errors.As(err, &parameterNotFound) || errors.As(err, &resourceNotFound)
+}
+
+type ProviderV2 struct {
+	access    awsV2AccessConfig
+	backend   *parameterStoreBackend
+	framework *framework.Framework
+}
+
+func NewProviderV2(f *framework.Framework) *ProviderV2 {
+	access := loadAWSV2AccessConfigFromEnv()
+	f.MakeRemoteRefKey = func(base string) string {
+		if f.Namespace == nil {
+			return parameterStoreRemoteRefKey(base, "")
+		}
+		return parameterStoreRemoteRefKey(base, f.Namespace.Name)
+	}
+
+	prov := &ProviderV2{
+		access:    access,
+		backend:   newParameterStoreBackend(access),
+		framework: f,
+	}
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			return
+		}
+		skipIfAWSV2StaticCredentialsMissing(access)
+	})
+
+	return prov
+}
+
+func parameterStoreRemoteRefKey(base, namespace string) string {
+	base = strings.Trim(base, "/")
+	if namespace == "" {
+		return "/e2e/" + base
+	}
+	return fmt.Sprintf("/e2e/%s/%s", namespace, base)
+}
+
+func (p *ProviderV2) CreateSecret(key string, val framework.SecretEntry) {
+	p.backend.CreateSecret(key, val)
+}
+
+func (p *ProviderV2) DeleteSecret(key string) {
+	p.backend.DeleteSecret(key)
+}
+
+func useV2StaticAuth(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider()
+	}
+}
+
+func (p *ProviderV2) prepareNamespacedProvider() func(*framework.TestCase, framework.SecretStoreProvider) {
+	return func(_ *framework.TestCase, _ framework.SecretStoreProvider) {
+		configName := p.providerConfigName()
+		createParameterStoreV2Config(p.framework, p.framework.Namespace.Name, configName, p.access)
+		frameworkv2.CreateProviderConnection(
+			p.framework,
+			p.framework.Namespace.Name,
+			p.framework.Namespace.Name,
+			frameworkv2.ProviderAddress("aws"),
+			awsProviderAPIVersion,
+			awsv2alpha1.ParameterStoreKind,
+			configName,
+			p.framework.Namespace.Name,
+		)
+		frameworkv2.WaitForProviderConnectionReady(p.framework, p.framework.Namespace.Name, p.framework.Namespace.Name, defaultV2WaitTimeout)
+	}
+}
+
+func (p *ProviderV2) providerConfigName() string {
+	return fmt.Sprintf("%s-parameterstore", p.framework.Namespace.Name)
+}
+
+func createParameterStoreV2ProviderConnection(f *framework.Framework, namespace, name, providerName, providerNamespace string) {
+	frameworkv2.CreateProviderConnection(
+		f,
+		namespace,
+		name,
+		frameworkv2.ProviderAddress("aws"),
+		awsProviderAPIVersion,
+		awsv2alpha1.ParameterStoreKind,
+		providerName,
+		providerNamespace,
+	)
+	log.Logf("created ParameterStore Provider connection: %s/%s", namespace, name)
+}

+ 56 - 0
e2e/suites/provider/cases/aws/parameterstore/provider_support_v2_test.go

@@ -0,0 +1,56 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"strings"
+	"testing"
+
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+)
+
+func TestNewParameterStoreV2ConfigUsesStaticSessionTokenSelector(t *testing.T) {
+	t.Parallel()
+
+	cfg := newParameterStoreV2Config("ns", "ps-static", awsV2AccessConfig{
+		Region: "eu-central-1",
+	})
+	if cfg.TypeMeta.Kind != awsv2alpha1.ParameterStoreKind {
+		t.Fatalf("expected kind %q, got %q", awsv2alpha1.ParameterStoreKind, cfg.TypeMeta.Kind)
+	}
+	if cfg.Spec.Auth.SecretRef == nil || cfg.Spec.Auth.SecretRef.SessionToken == nil {
+		t.Fatal("expected session token selector to be configured for static auth")
+	}
+	if cfg.Spec.Auth.SecretRef.SessionToken.Name != "ps-static-credentials" || cfg.Spec.Auth.SecretRef.SessionToken.Key != "st" {
+		t.Fatalf("unexpected session token selector: %+v", cfg.Spec.Auth.SecretRef.SessionToken)
+	}
+}
+
+func TestParameterStoreRemoteRefKeyAvoidsReservedPrefixes(t *testing.T) {
+	t.Parallel()
+
+	got := parameterStoreRemoteRefKey("aws-v2-ps-refresh-remote", "e2e-tests-eso-aws-ps-v2-6s27x")
+	if !strings.HasPrefix(got, "/e2e/") {
+		t.Fatalf("expected /e2e/ prefix, got %q", got)
+	}
+	if strings.HasPrefix(strings.TrimPrefix(got, "/"), "aws") || strings.HasPrefix(strings.TrimPrefix(got, "/"), "ssm") {
+		t.Fatalf("expected non-reserved parameter prefix, got %q", got)
+	}
+	if !strings.Contains(got, "aws-v2-ps-refresh-remote") {
+		t.Fatalf("expected remote key to retain base name, got %q", got)
+	}
+}

+ 120 - 0
e2e/suites/provider/cases/aws/parameterstore/provider_v2.go

@@ -0,0 +1,120 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+var _ = Describe("[aws] v2 namespaced provider", Label("aws", "parameterstore", "v2", "namespaced-provider"), func() {
+	f := framework.New("eso-aws-ps-v2")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("namespaced provider",
+		framework.TableFuncWithExternalSecret(f, prov),
+		framework.Compose(withStaticAuth, f, func(_ *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderSync(f, common.NamespacedProviderSyncConfig{
+				Description:        "[aws] should sync an ExternalSecret through a namespaced ParameterStore Provider using static credentials",
+				ExternalSecretName: "aws-v2-ps-static-es",
+				TargetSecretName:   "aws-v2-ps-static-target",
+				RemoteKey:          f.MakeRemoteRefKey("aws-v2-ps-static-remote"),
+				RemoteSecretValue:  "aws-v2-ps-static-value",
+				SecretKey:          "value",
+				ExpectedValue:      "aws-v2-ps-static-value",
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, func(_ *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderRefresh(f, common.NamespacedProviderRefreshConfig{
+				Description:         "[aws] should refresh synced ParameterStore secrets after the remote parameter changes",
+				ExternalSecretName:  "aws-v2-ps-refresh-es",
+				TargetSecretName:    "aws-v2-ps-refresh-target",
+				RemoteKey:           f.MakeRemoteRefKey("aws-v2-ps-refresh-remote"),
+				InitialSecretValue:  "aws-v2-ps-initial",
+				UpdatedSecretValue:  "aws-v2-ps-updated",
+				SecretKey:           "value",
+				InitialExpectedData: "aws-v2-ps-initial",
+				UpdatedExpectedData: "aws-v2-ps-updated",
+				RefreshInterval:     10 * time.Second,
+				WaitTimeout:         30 * time.Second,
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, FindByName, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, FindByTag, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, versionedParameterV2(prov), useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, common.StatusNotUpdatedAfterSuccessfulSync, useV2StaticAuth(prov)),
+	)
+})
+
+func versionedParameterV2(prov framework.SecretStoreProvider) func(*framework.Framework) (string, func(*framework.TestCase)) {
+	return func(f *framework.Framework) (string, func(*framework.TestCase)) {
+		return "[common] should read versioned secrets", func(tc *framework.TestCase) {
+			secretKey := fmt.Sprintf("/e2e/versioned/%s/%s", f.Namespace.Name, "one")
+			versions := []int{1, 2, 3, 4, 5}
+
+			tc.ExpectedSecret = commonVersionedExpectedSecret(versions)
+			tc.ExternalSecret.Spec.Data = commonVersionedExternalSecretData(secretKey, versions)
+			tc.Cleanup = func() {
+				prov.DeleteSecret(secretKey)
+			}
+
+			for _, version := range versions {
+				prov.CreateSecret(secretKey, framework.SecretEntry{
+					Value: fmt.Sprintf("value%d", version),
+				})
+			}
+		}
+	}
+}
+
+func commonVersionedExpectedSecret(versions []int) *corev1.Secret {
+	data := make(map[string][]byte, len(versions))
+	for _, version := range versions {
+		data[fmt.Sprintf("v%d", version)] = []byte(fmt.Sprintf("value%d", version))
+	}
+	return &corev1.Secret{
+		Type: corev1.SecretTypeOpaque,
+		Data: data,
+	}
+}
+
+func commonVersionedExternalSecretData(secretKey string, versions []int) []esapi.ExternalSecretData {
+	data := make([]esapi.ExternalSecretData, 0, len(versions))
+	for _, version := range versions {
+		data = append(data, esapi.ExternalSecretData{
+			SecretKey: fmt.Sprintf("v%d", version),
+			RemoteRef: esapi.ExternalSecretDataRemoteRef{
+				Key:     secretKey,
+				Version: fmt.Sprintf("%d", version),
+			},
+		})
+	}
+	return data
+}

+ 72 - 0
e2e/suites/provider/cases/aws/parameterstore/provider_v2_test.go

@@ -0,0 +1,72 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type recordingSecretStoreProvider struct {
+	created []string
+	deleted []string
+}
+
+func (p *recordingSecretStoreProvider) CreateSecret(key string, _ framework.SecretEntry) {
+	p.created = append(p.created, key)
+}
+
+func (p *recordingSecretStoreProvider) DeleteSecret(key string) {
+	p.deleted = append(p.deleted, key)
+}
+
+func TestVersionedParameterV2RegistersCleanupWithoutDeletingDuringSetup(t *testing.T) {
+	fakeProvider := &recordingSecretStoreProvider{}
+
+	f := &framework.Framework{
+		Namespace: &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{Name: "test-ns"},
+		},
+	}
+	_, tweak := versionedParameterV2(fakeProvider)(f)
+	tc := &framework.TestCase{
+		ExternalSecret: &esapi.ExternalSecret{},
+	}
+
+	tweak(tc)
+
+	if got, want := len(fakeProvider.created), 5; got != want {
+		t.Fatalf("expected %d created versions, got %d", want, got)
+	}
+	if got := len(fakeProvider.deleted); got != 0 {
+		t.Fatalf("expected no deletes during setup, got %d", got)
+	}
+	if tc.Cleanup == nil {
+		t.Fatalf("expected cleanup callback to be registered")
+	}
+
+	tc.Cleanup()
+
+	if got, want := len(fakeProvider.deleted), 1; got != want {
+		t.Fatalf("expected %d delete after cleanup, got %d", want, got)
+	}
+}

+ 152 - 0
e2e/suites/provider/cases/aws/parameterstore/push_v2.go

@@ -0,0 +1,152 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+var _ = Describe("[aws] v2 push secret", Label("aws", "parameterstore", "v2", "push-secret"), func() {
+	f := framework.New("eso-aws-ps-v2-push")
+	prov := NewProviderV2(f)
+	harness := newAWSClusterProviderPushHarness(f, prov)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("push secret",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		Entry(awsPushSecretImplicitProviderKind(f, prov)),
+		Entry(awsPushSecretRejectsNamespacedRemoteNamespaceOverride(f, prov)),
+		Entry(common.ClusterProviderPushManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderPushProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderPushDeniedByConditions(f, harness)),
+	)
+})
+
+func newAWSClusterProviderPushHarness(f *framework.Framework, prov *ProviderV2) common.ClusterProviderPushHarness {
+	return common.ClusterProviderPushHarness{
+		Prepare: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderPushRuntime {
+			s := newAWSClusterProviderScenario(f, cfg.Name, cfg.AuthScope, prov.access, prov.backend)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderPushRuntime{
+				ClusterProviderName:    clusterProviderName,
+				DefaultRemoteNamespace: "",
+				WaitForRemoteSecretValue: func(_, name, _ , expectedValue string) {
+					s.backend.WaitForSecretValue(name, expectedValue)
+				},
+				ExpectNoRemoteSecret: func(_, name string) {
+					s.backend.ExpectSecretAbsent(name)
+				},
+			}
+		},
+	}
+}
+
+func awsPushSecretImplicitProviderKind(f *framework.Framework, prov *ProviderV2) (string, func(*framework.TestCase)) {
+	return "[aws] should support namespaced Provider refs when push kind is omitted", func(tc *framework.TestCase) {
+		remoteKey := f.MakeRemoteRefKey("aws-v2-ps-push-implicit")
+		tc.Prepare = prov.prepareNamespacedProvider()
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "aws-v2-ps-push-implicit-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("value1"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "aws-v2-ps-push-implicit"
+		tc.PushSecret.Spec.DeletionPolicy = esv1alpha1.PushSecretDeletionPolicyDelete
+		tc.PushSecret.Spec.SecretStoreRefs[0].Kind = ""
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: remoteKey,
+					Property:  "value",
+				},
+			},
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			awscommon.WaitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+			prov.backend.WaitForSecretValue(remoteKey, "value1")
+
+			Expect(tc.Framework.CRClient.Delete(context.Background(), ps)).To(Succeed())
+			prov.backend.ExpectSecretAbsent(remoteKey)
+		}
+	}
+}
+
+func awsPushSecretRejectsNamespacedRemoteNamespaceOverride(f *framework.Framework, prov *ProviderV2) (string, func(*framework.TestCase)) {
+	return "[aws] should reject remote namespace overrides when pushing through a namespaced Provider", func(tc *framework.TestCase) {
+		remoteKey := f.MakeRemoteRefKey("aws-v2-ps-push-override")
+		tc.Prepare = prov.prepareNamespacedProvider()
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "aws-v2-ps-push-override-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("should-not-push"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "aws-v2-ps-push-override"
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: remoteKey,
+					Property:  "value",
+				},
+			},
+			Metadata: awscommon.PushSecretMetadataWithRemoteNamespace("ignored-aws-namespace"),
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			awscommon.WaitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+			prov.backend.ExpectSecretAbsent(remoteKey)
+			awscommon.ExpectPushSecretEventMessage(tc.Framework, ps.Namespace, ps.Name, `unknown field "remoteNamespace"`)
+		}
+	}
+}

+ 109 - 0
e2e/suites/provider/cases/aws/secretsmanager/clusterprovider_v2.go

@@ -0,0 +1,109 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+)
+
+var _ = Describe("[aws] v2 cluster provider", Label("aws", "secretsmanager", "v2", "cluster-provider"), func() {
+	f := framework.New("eso-aws-sm-v2-clusterprovider")
+	prov := NewProviderV2(f)
+	harness := newAWSClusterProviderExternalSecretHarness(f, prov)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("cluster provider external secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.ClusterProviderManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderDeniedByConditions(f, harness)),
+	)
+})
+
+type awsClusterProviderScenario struct {
+	common               awscommon.V2ClusterProviderScenario
+	access               awsAccessConfig
+	authScope            esv1.AuthenticationScope
+	backend              *secretsManagerBackend
+	f                    *framework.Framework
+}
+
+func newAWSClusterProviderScenario(f *framework.Framework, prefix string, authScope esv1.AuthenticationScope, access awsAccessConfig, backend *secretsManagerBackend) *awsClusterProviderScenario {
+	shared := awscommon.NewV2ClusterProviderScenario(f.Namespace.Name, prefix, authScope, func(prefix string) string {
+		return common.CreateProviderCaseNamespace(f, prefix, defaultV2PollInterval)
+	})
+	s := &awsClusterProviderScenario{
+		common:    shared,
+		access:    access,
+		authScope: authScope,
+		backend:   backend,
+		f:         f,
+	}
+	createSecretsManagerV2Config(s.f, s.common.ConfigNamespace, s.common.ConfigName, s.access, awsAuthProfileStatic)
+	return s
+}
+
+func (s *awsClusterProviderScenario) createClusterProvider(conditions []esv1.ClusterSecretStoreCondition) string {
+	clusterProviderName := s.common.ClusterProviderName()
+	frameworkv2.CreateClusterProviderConnection(
+		s.f,
+		clusterProviderName,
+		frameworkv2.ProviderAddress("aws"),
+		awsProviderAPIVersion,
+		awsv2alpha1.SecretsManagerKind,
+		s.common.ConfigName,
+		s.common.ProviderRefNamespace,
+		s.common.AuthScope,
+		conditions,
+	)
+	return clusterProviderName
+}
+
+func (s *awsClusterProviderScenario) CreateSecret(key string, val framework.SecretEntry) {
+	s.backend.CreateSecret(key, val)
+}
+
+func (s *awsClusterProviderScenario) DeleteSecret(key string) {
+	s.backend.DeleteSecret(key)
+}
+
+func newAWSClusterProviderExternalSecretHarness(f *framework.Framework, prov *ProviderV2) common.ClusterProviderExternalSecretHarness {
+	return common.ClusterProviderExternalSecretHarness{
+		Prepare: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderExternalSecretRuntime {
+			s := newAWSClusterProviderScenario(f, cfg.Name, cfg.AuthScope, prov.access, prov.backend)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderExternalSecretRuntime{
+				ClusterProviderName: clusterProviderName,
+				Provider:            s,
+			}
+		},
+	}
+}

+ 15 - 63
e2e/suites/provider/cases/aws/secretsmanager/provider.go

@@ -17,17 +17,6 @@ limitations under the License.
 package aws
 package aws
 
 
 import (
 import (
-	"context"
-	"errors"
-	"os"
-	"time"
-
-	"github.com/aws/aws-sdk-go-v2/aws"
-	"github.com/aws/aws-sdk-go-v2/config"
-	"github.com/aws/aws-sdk-go-v2/credentials"
-	"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
-	secretsmanagertypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
-
 	//nolint
 	//nolint
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/ginkgo/v2"
 
 
@@ -47,26 +36,30 @@ type Provider struct {
 	ServiceAccountName      string
 	ServiceAccountName      string
 	ServiceAccountNamespace string
 	ServiceAccountNamespace string
 
 
+	backend   *secretsManagerBackend
 	region    string
 	region    string
-	client    *secretsmanager.Client
 	framework *framework.Framework
 	framework *framework.Framework
 }
 }
 
 
 func NewProvider(f *framework.Framework, kid, sak, st, region, saName, saNamespace string) *Provider {
 func NewProvider(f *framework.Framework, kid, sak, st, region, saName, saNamespace string) *Provider {
+	access := awsAccessConfig{
+		KID:         kid,
+		SAK:         sak,
+		ST:          st,
+		Region:      region,
+		SAName:      saName,
+		SANamespace: saNamespace,
+	}
 	prov := &Provider{
 	prov := &Provider{
 		ServiceAccountName:      saName,
 		ServiceAccountName:      saName,
 		ServiceAccountNamespace: saNamespace,
 		ServiceAccountNamespace: saNamespace,
+		backend:                 newSecretsManagerBackend(f, access),
 		region:                  region,
 		region:                  region,
 		framework:               f,
 		framework:               f,
 	}
 	}
 
 
-	BeforeAll(func() {
-		config, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(kid, sak, st)))
-		Expect(err).ToNot(HaveOccurred())
-		prov.client = secretsmanager.NewFromConfig(config)
-	})
-
 	BeforeEach(func() {
 	BeforeEach(func() {
+		skipIfAWSStaticCredentialsMissing(access)
 		awscommon.SetupStaticStore(f, awscommon.AccessOpts{KID: kid, SAK: sak, ST: st, Region: region}, esv1.AWSServiceSecretsManager)
 		awscommon.SetupStaticStore(f, awscommon.AccessOpts{KID: kid, SAK: sak, ST: st, Region: region}, esv1.AWSServiceSecretsManager)
 		awscommon.SetupExternalIDStore(
 		awscommon.SetupExternalIDStore(
 			f,
 			f,
@@ -95,61 +88,20 @@ func NewProvider(f *framework.Framework, kid, sak, st, region, saName, saNamespa
 }
 }
 
 
 func NewFromEnv(f *framework.Framework) *Provider {
 func NewFromEnv(f *framework.Framework) *Provider {
-	kid := os.Getenv("AWS_ACCESS_KEY_ID")
-	sak := os.Getenv("AWS_SECRET_ACCESS_KEY")
-	st := os.Getenv("AWS_SESSION_TOKEN")
-	region := os.Getenv("AWS_REGION")
-	saName := os.Getenv("AWS_SA_NAME")
-	saNamespace := os.Getenv("AWS_SA_NAMESPACE")
-	return NewProvider(f, kid, sak, st, region, saName, saNamespace)
+	access := loadAWSAccessConfigFromEnv()
+	return NewProvider(f, access.KID, access.SAK, access.ST, access.Region, access.SAName, access.SANamespace)
 }
 }
 
 
 // CreateSecret creates a secret at the provider.
 // CreateSecret creates a secret at the provider.
 func (s *Provider) CreateSecret(key string, val framework.SecretEntry) {
 func (s *Provider) CreateSecret(key string, val framework.SecretEntry) {
-	smTags := make([]secretsmanagertypes.Tag, 0)
-	for k, v := range val.Tags {
-		smTags = append(smTags, secretsmanagertypes.Tag{
-			Key:   aws.String(k),
-			Value: aws.String(v),
-		})
-	}
-
-	// we re-use some secret names throughout our test suite
-	// due to the fact that there is a short delay before the secret is actually deleted
-	// we have to retry creating the secret
-	attempts := 20
-	for {
-		log.Logf("creating secret %s / attempts left: %d", key, attempts)
-		_, err := s.client.CreateSecret(GinkgoT().Context(), &secretsmanager.CreateSecretInput{
-			Name:         aws.String(key),
-			SecretString: aws.String(val.Value),
-			Tags:         smTags,
-		})
-		if err == nil {
-			return
-		}
-		attempts--
-		if attempts < 0 {
-			Fail("unable to create secret: " + err.Error())
-		}
-		<-time.After(time.Second * 5)
-	}
+	s.backend.CreateSecret(key, val)
 }
 }
 
 
 // DeleteSecret deletes a secret at the provider.
 // DeleteSecret deletes a secret at the provider.
 // There may be a short delay between calling this function
 // There may be a short delay between calling this function
 // and the removal of the secret on the provider side.
 // and the removal of the secret on the provider side.
 func (s *Provider) DeleteSecret(key string) {
 func (s *Provider) DeleteSecret(key string) {
-	log.Logf("deleting secret %s", key)
-	_, err := s.client.DeleteSecret(GinkgoT().Context(), &secretsmanager.DeleteSecretInput{
-		SecretId:                   aws.String(key),
-		ForceDeleteWithoutRecovery: aws.Bool(true),
-	})
-	var nf *secretsmanagertypes.ResourceNotFoundException
-	if errors.As(err, &nf) {
-		return
-	}
-	Expect(err).ToNot(HaveOccurred())
+	s.backend.DeleteSecret(key)
 }
 }
 
 
 // MountedIRSAStore is a SecretStore without auth config
 // MountedIRSAStore is a SecretStore without auth config

+ 484 - 0
e2e/suites/provider/cases/aws/secretsmanager/provider_support.go

@@ -0,0 +1,484 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	secretsmanagertypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/framework/log"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+)
+
+const (
+	awsProviderAPIVersion = "provider.external-secrets.io/v2alpha1"
+	defaultV2WaitTimeout  = 60 * time.Second
+	defaultV2PollInterval = 2 * time.Second
+)
+
+const (
+	assumeRoleSessionName    = "eso-e2e-probe"
+)
+
+type awsAuthProfile string
+
+const (
+	awsAuthProfileStatic         awsAuthProfile = "static"
+	awsAuthProfileExternalID     awsAuthProfile = "external-id"
+	awsAuthProfileSessionTags    awsAuthProfile = "session-tags"
+	awsAuthProfileReferencedIRSA awsAuthProfile = "referenced-irsa"
+	awsAuthProfileMountedIRSA    awsAuthProfile = "mounted-irsa"
+)
+
+type awsAccessConfig struct {
+	KID         string
+	SAK         string
+	ST          string
+	Region      string
+	Role        string
+	SAName      string
+	SANamespace string
+}
+
+type secretsManagerBackend struct {
+	access     awsAccessConfig
+	client     *awssm.Client
+	clientErr  error
+	clientOnce sync.Once
+	framework  *framework.Framework
+}
+
+type stsAssumeRoleClient interface {
+	AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
+}
+
+type assumeRoleProbeKey struct {
+	access  awsAccessConfig
+	profile awsAuthProfile
+}
+
+type assumeRoleProbeResult struct {
+	err error
+}
+
+var assumeRoleProbeCache sync.Map
+
+func loadAWSAccessConfigFromEnv() awsAccessConfig {
+	return awsAccessConfig{
+		KID:         os.Getenv("AWS_ACCESS_KEY_ID"),
+		SAK:         os.Getenv("AWS_SECRET_ACCESS_KEY"),
+		ST:          os.Getenv("AWS_SESSION_TOKEN"),
+		Region:      os.Getenv("AWS_REGION"),
+		SAName:      os.Getenv("AWS_SA_NAME"),
+		SANamespace: os.Getenv("AWS_SA_NAMESPACE"),
+	}
+}
+
+func newBackendFromEnv(f *framework.Framework) *secretsManagerBackend {
+	return newSecretsManagerBackend(f, loadAWSAccessConfigFromEnv())
+}
+
+func newSecretsManagerBackend(f *framework.Framework, access awsAccessConfig) *secretsManagerBackend {
+	return &secretsManagerBackend{
+		access:    access,
+		framework: f,
+	}
+}
+
+func (c awsAccessConfig) missingStaticCredentials() []string {
+	var missing []string
+	if c.KID == "" {
+		missing = append(missing, "AWS_ACCESS_KEY_ID")
+	}
+	if c.SAK == "" {
+		missing = append(missing, "AWS_SECRET_ACCESS_KEY")
+	}
+	if c.Region == "" {
+		missing = append(missing, "AWS_REGION")
+	}
+	return missing
+}
+
+func skipIfAWSStaticCredentialsMissing(access awsAccessConfig) {
+	if missing := access.missingStaticCredentials(); len(missing) > 0 {
+		Skip("missing AWS e2e credentials: " + strings.Join(missing, ", "))
+	}
+}
+
+func skipIfAWSManagedIRSAEnvMissing(access awsAccessConfig) {
+	var missing []string
+	if access.Region == "" {
+		missing = append(missing, "AWS_REGION")
+	}
+	if access.SAName == "" {
+		missing = append(missing, "AWS_SA_NAME")
+	}
+	if access.SANamespace == "" {
+		missing = append(missing, "AWS_SA_NAMESPACE")
+	}
+	if len(missing) > 0 {
+		Skip("missing AWS managed IRSA environment: " + strings.Join(missing, ", "))
+	}
+}
+
+func skipIfAWSAssumeRoleProbeDenied(access awsAccessConfig, profile awsAuthProfile) {
+	if profile != awsAuthProfileExternalID && profile != awsAuthProfileSessionTags {
+		return
+	}
+
+	cacheKey := assumeRoleProbeKey{
+		access:  access,
+		profile: profile,
+	}
+	if cached, ok := assumeRoleProbeCache.Load(cacheKey); ok {
+		handleAssumeRoleProbeResult(access, profile, cached.(assumeRoleProbeResult).err)
+		return
+	}
+
+	cfg, err := loadAWSConfig(access)
+	Expect(err).NotTo(HaveOccurred())
+
+	err = probeAssumeRoleAccess(context.Background(), sts.NewFromConfig(cfg), access, profile)
+	assumeRoleProbeCache.Store(cacheKey, assumeRoleProbeResult{err: err})
+	handleAssumeRoleProbeResult(access, profile, err)
+}
+
+func handleAssumeRoleProbeResult(access awsAccessConfig, profile awsAuthProfile, err error) {
+	if err == nil {
+		return
+	}
+	if isAssumeRoleAccessDenied(err) {
+		Skip(fmt.Sprintf("skipping AWS %s auth e2e: %s is not authorized to assume role %q with the current credentials", profile, assumeRoleAction(profile), roleARNForProfile(access, profile)))
+	}
+	Expect(err).NotTo(HaveOccurred())
+}
+
+func assumeRoleAction(profile awsAuthProfile) string {
+	if profile == awsAuthProfileSessionTags {
+		return "sts:TagSession"
+	}
+	return "sts:AssumeRole"
+}
+
+func staticAWSAuth(secretName string) esv1.AWSAuth {
+	return esv1.AWSAuth{
+		SecretRef: &esv1.AWSAuthSecretRef{
+			AccessKeyID: esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticAccessKeyIDKey,
+			},
+			SecretAccessKey: esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticSecretAccessKeyKey,
+			},
+			SessionToken: &esmetav1.SecretKeySelector{
+				Name: secretName,
+				Key:  awscommon.StaticSessionTokenKey,
+			},
+		},
+	}
+}
+
+func newStaticCredentialsSecret(namespace, name string, access awsAccessConfig) *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		StringData: awscommon.StaticCredentialsSecretData(access.KID, access.SAK, access.ST),
+	}
+}
+
+func createStaticCredentialsSecret(f *framework.Framework, namespace, name string, access awsAccessConfig) {
+	Expect(f.CRClient.Create(GinkgoT().Context(), newStaticCredentialsSecret(namespace, name, access))).To(Succeed())
+}
+
+func roleARNForProfile(access awsAccessConfig, profile awsAuthProfile) string {
+	if access.Role != "" {
+		return access.Role
+	}
+	switch profile {
+	case awsAuthProfileExternalID:
+		return awscommon.IAMRoleExternalID
+	case awsAuthProfileSessionTags:
+		return awscommon.IAMRoleSessionTags
+	default:
+		return ""
+	}
+}
+
+func sessionTagsForProfile(profile awsAuthProfile) []ststypes.Tag {
+	if profile != awsAuthProfileSessionTags {
+		return nil
+	}
+
+	return []ststypes.Tag{{
+		Key:   aws.String("namespace"),
+		Value: aws.String("e2e-test"),
+	}}
+}
+
+func probeAssumeRoleAccess(ctx context.Context, client stsAssumeRoleClient, access awsAccessConfig, profile awsAuthProfile) error {
+	if profile != awsAuthProfileExternalID && profile != awsAuthProfileSessionTags {
+		return nil
+	}
+
+	input := &sts.AssumeRoleInput{
+		RoleArn:         aws.String(roleARNForProfile(access, profile)),
+		RoleSessionName: aws.String(assumeRoleSessionName),
+		Tags:            sessionTagsForProfile(profile),
+	}
+	if profile == awsAuthProfileExternalID {
+		input.ExternalId = aws.String(awscommon.IAMTrustedExternalID)
+	}
+
+	_, err := client.AssumeRole(ctx, input)
+	return err
+}
+
+func isAssumeRoleAccessDenied(err error) bool {
+	if err == nil {
+		return false
+	}
+
+	msg := strings.ToLower(err.Error())
+	if !strings.Contains(msg, "accessdenied") {
+		return false
+	}
+	return strings.Contains(msg, "sts:assumerole") || strings.Contains(msg, "sts:tagsession")
+}
+
+func newSecretsManagerV2Config(namespace, name string, access awsAccessConfig, profile awsAuthProfile) *awsv2alpha1.SecretsManager {
+	cfg := &awsv2alpha1.SecretsManager{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: awsv2alpha1.GroupVersion.String(),
+			Kind:       awsv2alpha1.SecretsManagerKind,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: awsv2alpha1.SecretsManagerSpec{
+			Region: access.Region,
+		},
+	}
+
+	switch profile {
+	case awsAuthProfileStatic:
+		cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
+	case awsAuthProfileExternalID:
+		cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
+		cfg.Spec.Role = access.Role
+		if cfg.Spec.Role == "" {
+			cfg.Spec.Role = awscommon.IAMRoleExternalID
+		}
+		cfg.Spec.ExternalID = awscommon.IAMTrustedExternalID
+	case awsAuthProfileSessionTags:
+		cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
+		cfg.Spec.Role = access.Role
+		if cfg.Spec.Role == "" {
+			cfg.Spec.Role = awscommon.IAMRoleSessionTags
+		}
+		cfg.Spec.SessionTags = []*esv1.Tag{{
+			Key:   "namespace",
+			Value: "e2e-test",
+		}}
+	case awsAuthProfileReferencedIRSA:
+		cfg.Spec.Auth = esv1.AWSAuth{
+			JWTAuth: &esv1.AWSJWTAuth{
+				ServiceAccountRef: &esmetav1.ServiceAccountSelector{
+					Name:      access.SAName,
+					Namespace: &access.SANamespace,
+				},
+			},
+		}
+	case awsAuthProfileMountedIRSA:
+		cfg.Spec.Auth = esv1.AWSAuth{}
+	default:
+		cfg.Spec.Auth = staticAWSAuth(awscommon.CredentialsSecretName(name))
+	}
+
+	return cfg
+}
+
+func createSecretsManagerV2Config(f *framework.Framework, namespace, name string, access awsAccessConfig, profile awsAuthProfile) *awsv2alpha1.SecretsManager {
+	if profile == awsAuthProfileStatic || profile == awsAuthProfileExternalID || profile == awsAuthProfileSessionTags {
+		createStaticCredentialsSecret(f, namespace, awscommon.CredentialsSecretName(name), access)
+	}
+
+	cfg := newSecretsManagerV2Config(namespace, name, access, profile)
+	Expect(f.CRClient.Create(GinkgoT().Context(), cfg)).To(Succeed())
+	return cfg
+}
+
+func createSecretsManagerV2ProviderConnection(f *framework.Framework, namespace, name, providerName, providerNamespace string) {
+	frameworkv2.CreateProviderConnection(
+		f,
+		namespace,
+		name,
+		frameworkv2.ProviderAddress("aws"),
+		awsProviderAPIVersion,
+		awsv2alpha1.SecretsManagerKind,
+		providerName,
+		providerNamespace,
+	)
+}
+
+func loadAWSConfig(access awsAccessConfig) (aws.Config, error) {
+	loadOptions := []func(*config.LoadOptions) error{
+		config.WithRegion(access.Region),
+	}
+	if access.KID != "" || access.SAK != "" || access.ST != "" {
+		loadOptions = append(loadOptions, config.WithCredentialsProvider(
+			credentials.NewStaticCredentialsProvider(access.KID, access.SAK, access.ST),
+		))
+	}
+
+	return config.LoadDefaultConfig(context.Background(), loadOptions...)
+}
+
+func (b *secretsManagerBackend) ensureClient() {
+	b.clientOnce.Do(func() {
+		cfg, err := loadAWSConfig(b.access)
+		if err != nil {
+			b.clientErr = err
+			return
+		}
+		b.client = awssm.NewFromConfig(cfg)
+	})
+
+	Expect(b.clientErr).ToNot(HaveOccurred())
+	Expect(b.client).NotTo(BeNil())
+}
+
+func (b *secretsManagerBackend) CreateSecret(key string, val framework.SecretEntry) {
+	b.ensureClient()
+
+	smTags := make([]secretsmanagertypes.Tag, 0, len(val.Tags))
+	for tagKey, tagValue := range val.Tags {
+		smTags = append(smTags, secretsmanagertypes.Tag{
+			Key:   aws.String(tagKey),
+			Value: aws.String(tagValue),
+		})
+	}
+
+	attempts := 20
+	for {
+		log.Logf("creating secret %s / attempts left: %d", key, attempts)
+		_, err := b.client.CreateSecret(GinkgoT().Context(), &awssm.CreateSecretInput{
+			Name:         aws.String(key),
+			SecretString: aws.String(val.Value),
+			Tags:         smTags,
+		})
+		if err == nil {
+			return
+		}
+		attempts--
+		if attempts < 0 {
+			Fail("unable to create secret: " + err.Error())
+		}
+		<-time.After(5 * time.Second)
+	}
+}
+
+func (b *secretsManagerBackend) DeleteSecret(key string) {
+	b.ensureClient()
+
+	log.Logf("deleting secret %s", key)
+	_, err := b.client.DeleteSecret(GinkgoT().Context(), &awssm.DeleteSecretInput{
+		SecretId:                   aws.String(key),
+		ForceDeleteWithoutRecovery: aws.Bool(true),
+	})
+	var notFound *secretsmanagertypes.ResourceNotFoundException
+	if errors.As(err, &notFound) {
+		return
+	}
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (b *secretsManagerBackend) WaitForSecretValue(name, expectedValue string) {
+	b.ensureClient()
+
+	Eventually(func(g Gomega) {
+		out, err := b.client.GetSecretValue(GinkgoT().Context(), &awssm.GetSecretValueInput{
+			SecretId: aws.String(name),
+		})
+		g.Expect(err).NotTo(HaveOccurred())
+		g.Expect(secretValueString(out)).To(Equal(expectedValue))
+	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(Succeed())
+}
+
+func (b *secretsManagerBackend) ExpectSecretAbsent(name string) {
+	b.ensureClient()
+
+	Eventually(func() bool {
+		_, err := b.client.GetSecretValue(GinkgoT().Context(), &awssm.GetSecretValueInput{
+			SecretId: aws.String(name),
+		})
+		return secretReadErrorIndicatesAbsence(err)
+	}, defaultV2WaitTimeout, defaultV2PollInterval).Should(BeTrue(), fmt.Sprintf("expected AWS secret %q to be absent", name))
+}
+
+func secretValueString(out *awssm.GetSecretValueOutput) string {
+	if out == nil {
+		return ""
+	}
+	if out.SecretString != nil {
+		return aws.ToString(out.SecretString)
+	}
+	if len(out.SecretBinary) > 0 {
+		return string(out.SecretBinary)
+	}
+	return ""
+}
+
+func secretReadErrorIndicatesAbsence(err error) bool {
+	if err == nil {
+		return false
+	}
+
+	var notFound *secretsmanagertypes.ResourceNotFoundException
+	if errors.As(err, &notFound) {
+		return true
+	}
+
+	msg := strings.ToLower(err.Error())
+	return strings.Contains(msg, "marked for deletion") || strings.Contains(msg, "scheduled for deletion")
+}

+ 210 - 0
e2e/suites/provider/cases/aws/secretsmanager/provider_support_test.go

@@ -0,0 +1,210 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+	"github.com/aws/aws-sdk-go-v2/service/sts"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type fakeSTSAssumeRoleClient struct {
+	input *sts.AssumeRoleInput
+	err   error
+}
+
+func (f *fakeSTSAssumeRoleClient) AssumeRole(_ context.Context, input *sts.AssumeRoleInput, _ ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) {
+	f.input = input
+	if f.err != nil {
+		return nil, f.err
+	}
+	return &sts.AssumeRoleOutput{}, nil
+}
+
+func TestProviderAddressInNamespace(t *testing.T) {
+	t.Parallel()
+
+	got := frameworkv2.ProviderAddressInNamespace("aws", "aws-irsa-system")
+	if got != "provider-aws.aws-irsa-system.svc:8080" {
+		t.Fatalf("unexpected address: %s", got)
+	}
+}
+
+func TestStaticAWSAuthUsesSessionTokenSelector(t *testing.T) {
+	t.Parallel()
+
+	auth := staticAWSAuth("aws-creds")
+	if auth.SecretRef == nil || auth.SecretRef.SessionToken == nil {
+		t.Fatal("expected session token selector to be preserved")
+	}
+	if auth.SecretRef.SessionToken.Name != "aws-creds" || auth.SecretRef.SessionToken.Key != "st" {
+		t.Fatalf("unexpected session token selector: %+v", auth.SecretRef.SessionToken)
+	}
+}
+
+func TestSecretsManagerConfigForExternalID(t *testing.T) {
+	t.Parallel()
+
+	cfg := newSecretsManagerV2Config("ns", "sm-extid", awsAccessConfig{
+		Region: "eu-west-1",
+		Role:   awscommon.IAMRoleExternalID,
+	}, awsAuthProfileExternalID)
+	if cfg.Spec.ExternalID != awscommon.IAMTrustedExternalID {
+		t.Fatalf("expected external ID %q, got %q", awscommon.IAMTrustedExternalID, cfg.Spec.ExternalID)
+	}
+}
+
+func TestSecretsManagerConfigForSessionTags(t *testing.T) {
+	t.Parallel()
+
+	cfg := newSecretsManagerV2Config("ns", "sm-tags", awsAccessConfig{
+		Region: "eu-west-1",
+		Role:   awscommon.IAMRoleSessionTags,
+	}, awsAuthProfileSessionTags)
+	if len(cfg.Spec.SessionTags) != 1 {
+		t.Fatalf("expected one session tag, got %d", len(cfg.Spec.SessionTags))
+	}
+	if cfg.Spec.SessionTags[0].Key != "namespace" || cfg.Spec.SessionTags[0].Value != "e2e-test" {
+		t.Fatalf("unexpected session tags: %+v", cfg.Spec.SessionTags)
+	}
+}
+
+func TestProviderConfigNamespaceForManifestScope(t *testing.T) {
+	t.Parallel()
+
+	if got := awscommon.ProviderConfigNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns", "workload-ns"); got != "workload-ns" {
+		t.Fatalf("expected workload namespace, got %q", got)
+	}
+}
+
+func TestProviderConfigNamespaceForProviderScope(t *testing.T) {
+	t.Parallel()
+
+	if got := awscommon.ProviderConfigNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns", "workload-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace, got %q", got)
+	}
+}
+
+func TestProviderReferenceNamespaceForManifestScope(t *testing.T) {
+	t.Parallel()
+
+	if got := awscommon.ProviderReferenceNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns"); got != "" {
+		t.Fatalf("expected empty provider reference namespace, got %q", got)
+	}
+}
+
+func TestProviderReferenceNamespaceForProviderScope(t *testing.T) {
+	t.Parallel()
+
+	if got := awscommon.ProviderReferenceNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace, got %q", got)
+	}
+}
+
+func TestSecretValueStringUsesSecretString(t *testing.T) {
+	t.Parallel()
+
+	got := secretValueString(&awssm.GetSecretValueOutput{
+		SecretString: aws.String(`{"value":"from-string"}`),
+		SecretBinary: []byte(`{"value":"from-binary"}`),
+	})
+	if got != `{"value":"from-string"}` {
+		t.Fatalf("expected SecretString payload, got %q", got)
+	}
+}
+
+func TestSecretValueStringFallsBackToSecretBinary(t *testing.T) {
+	t.Parallel()
+
+	got := secretValueString(&awssm.GetSecretValueOutput{
+		SecretBinary: []byte(`{"value":"from-binary"}`),
+	})
+	if got != `{"value":"from-binary"}` {
+		t.Fatalf("expected SecretBinary payload, got %q", got)
+	}
+}
+
+func TestSecretReadErrorIndicatesAbsenceRecognizesMarkedForDeletion(t *testing.T) {
+	t.Parallel()
+
+	err := errors.New("InvalidRequestException: You can't perform this operation on the secret because it was marked for deletion.")
+	if !secretReadErrorIndicatesAbsence(err) {
+		t.Fatal("expected marked-for-deletion error to be treated as absence")
+	}
+}
+
+func TestProbeAssumeRoleAccessBuildsExternalIDRequest(t *testing.T) {
+	t.Parallel()
+
+	client := &fakeSTSAssumeRoleClient{}
+	access := awsAccessConfig{
+		Role: awscommon.IAMRoleExternalID,
+	}
+	if err := probeAssumeRoleAccess(context.Background(), client, access, awsAuthProfileExternalID); err != nil {
+		t.Fatalf("probeAssumeRoleAccess() error = %v", err)
+	}
+	if client.input == nil {
+		t.Fatal("expected AssumeRole input to be recorded")
+	}
+	if got := aws.ToString(client.input.RoleArn); got != awscommon.IAMRoleExternalID {
+		t.Fatalf("expected role ARN %q, got %q", awscommon.IAMRoleExternalID, got)
+	}
+	if got := aws.ToString(client.input.ExternalId); got != awscommon.IAMTrustedExternalID {
+		t.Fatalf("expected external ID %q, got %q", awscommon.IAMTrustedExternalID, got)
+	}
+}
+
+func TestProbeAssumeRoleAccessBuildsSessionTagsRequest(t *testing.T) {
+	t.Parallel()
+
+	client := &fakeSTSAssumeRoleClient{}
+	access := awsAccessConfig{
+		Role: awscommon.IAMRoleSessionTags,
+	}
+	if err := probeAssumeRoleAccess(context.Background(), client, access, awsAuthProfileSessionTags); err != nil {
+		t.Fatalf("probeAssumeRoleAccess() error = %v", err)
+	}
+	if client.input == nil {
+		t.Fatal("expected AssumeRole input to be recorded")
+	}
+	if got := aws.ToString(client.input.RoleArn); got != awscommon.IAMRoleSessionTags {
+		t.Fatalf("expected role ARN %q, got %q", awscommon.IAMRoleSessionTags, got)
+	}
+	if len(client.input.Tags) != 1 {
+		t.Fatalf("expected one session tag, got %d", len(client.input.Tags))
+	}
+	tag := client.input.Tags[0]
+	if aws.ToString(tag.Key) != "namespace" || aws.ToString(tag.Value) != "e2e-test" {
+		t.Fatalf("unexpected session tag: %+v", tag)
+	}
+}
+
+func TestIsAssumeRoleAccessDeniedRecognizesSTSAccessDeniedErrors(t *testing.T) {
+	t.Parallel()
+
+	err := errors.New("api error AccessDenied: User is not authorized to perform: sts:TagSession")
+	if !isAssumeRoleAccessDenied(err) {
+		t.Fatal("expected sts access denied error to be recognized")
+	}
+}

+ 181 - 0
e2e/suites/provider/cases/aws/secretsmanager/provider_v2.go

@@ -0,0 +1,181 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+)
+
+var _ = Describe("[aws] v2 namespaced provider", Label("aws", "secretsmanager", "v2", "namespaced-provider"), func() {
+	f := framework.New("eso-aws-sm-v2")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("namespaced provider",
+		framework.TableFuncWithExternalSecret(f, prov),
+		framework.Compose(withStaticAuth, f, func(f *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderSync(f, common.NamespacedProviderSyncConfig{
+				Description:        "[aws] should sync an ExternalSecret through a namespaced Provider using static credentials",
+				ExternalSecretName: "aws-v2-static-es",
+				TargetSecretName:   "aws-v2-static-target",
+				RemoteKey:          f.MakeRemoteRefKey("aws-v2-static-remote"),
+				RemoteSecretValue:  `{"value":"aws-v2-static-value"}`,
+				RemoteProperty:     "value",
+				SecretKey:          "value",
+				ExpectedValue:      "aws-v2-static-value",
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, func(f *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderRefresh(f, common.NamespacedProviderRefreshConfig{
+				Description:         "[aws] should refresh synced secrets after the remote AWS secret changes",
+				ExternalSecretName:  "aws-v2-refresh-es",
+				TargetSecretName:    "aws-v2-refresh-target",
+				RemoteKey:           f.MakeRemoteRefKey("aws-v2-refresh-remote"),
+				InitialSecretValue:  `{"value":"aws-v2-initial"}`,
+				UpdatedSecretValue:  `{"value":"aws-v2-updated"}`,
+				RemoteProperty:      "value",
+				SecretKey:           "value",
+				InitialExpectedData: "aws-v2-initial",
+				UpdatedExpectedData: "aws-v2-updated",
+				RefreshInterval:     10 * time.Second,
+				WaitTimeout:         30 * time.Second,
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, func(f *framework.Framework) (string, func(*framework.TestCase)) {
+			return common.NamespacedProviderFind(f, common.NamespacedProviderFindConfig{
+				Description:        "[aws] should sync ExternalSecret dataFrom.find through a namespaced Provider",
+				ExternalSecretName: "aws-v2-find-es",
+				TargetSecretName:   "aws-v2-find-target",
+				MatchRegExp:        "^aws-v2-find-(one|two)$",
+				MatchingSecrets: map[string]string{
+					"aws-v2-find-one": "aws-v2-one",
+					"aws-v2-find-two": "aws-v2-two",
+				},
+				IgnoredSecrets: map[string]string{
+					"aws-v2-ignore": "aws-v2-ignore",
+				},
+			})
+		}, useV2StaticAuth(prov)),
+		framework.Compose(withStaticAuth, f, common.StatusNotUpdatedAfterSuccessfulSync, useV2StaticAuth(prov)),
+		framework.Compose(withExtID, f, SimpleSyncWithNamespaceTags(nil), useV2ExternalIDAuth(prov)),
+		framework.Compose(withSessionTags, f, SimpleSyncWithNamespaceTags(nil), useV2SessionTagsAuth(prov)),
+	)
+})
+
+type ProviderV2 struct {
+	access    awsAccessConfig
+	backend   *secretsManagerBackend
+	framework *framework.Framework
+}
+
+func NewProviderV2(f *framework.Framework) *ProviderV2 {
+	access := loadAWSAccessConfigFromEnv()
+	f.MakeRemoteRefKey = func(base string) string {
+		if f.Namespace == nil {
+			return base
+		}
+		suffix := f.Namespace.Name
+		if len(suffix) > 8 {
+			suffix = suffix[len(suffix)-8:]
+		}
+		if suffix == "" {
+			return base
+		}
+		return fmt.Sprintf("%s-%s", base, suffix)
+	}
+	prov := &ProviderV2{
+		access:    access,
+		backend:   newSecretsManagerBackend(f, access),
+		framework: f,
+	}
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			return
+		}
+		skipIfAWSStaticCredentialsMissing(access)
+	})
+
+	return prov
+}
+
+func (p *ProviderV2) CreateSecret(key string, val framework.SecretEntry) {
+	p.backend.CreateSecret(key, val)
+}
+
+func (p *ProviderV2) DeleteSecret(key string) {
+	p.backend.DeleteSecret(key)
+}
+
+func useV2StaticAuth(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileStatic)
+	}
+}
+
+func useV2ExternalIDAuth(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileExternalID)
+	}
+}
+
+func useV2SessionTagsAuth(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileSessionTags)
+	}
+}
+
+func (p *ProviderV2) prepareNamespacedProvider(profile awsAuthProfile) func(*framework.TestCase, framework.SecretStoreProvider) {
+	return p.prepareNamespacedProviderAtAddress(profile, frameworkv2.ProviderAddress("aws"))
+}
+
+func (p *ProviderV2) prepareNamespacedProviderAtAddress(profile awsAuthProfile, address string) func(*framework.TestCase, framework.SecretStoreProvider) {
+	return func(_ *framework.TestCase, _ framework.SecretStoreProvider) {
+		skipIfAWSAssumeRoleProbeDenied(p.access, profile)
+
+		configName := p.providerConfigName(profile)
+		createSecretsManagerV2Config(p.framework, p.framework.Namespace.Name, configName, p.access, profile)
+		frameworkv2.CreateProviderConnection(
+			p.framework,
+			p.framework.Namespace.Name,
+			p.framework.Namespace.Name,
+			address,
+			awsProviderAPIVersion,
+			awsv2alpha1.SecretsManagerKind,
+			configName,
+			p.framework.Namespace.Name,
+		)
+		frameworkv2.WaitForProviderConnectionReady(p.framework, p.framework.Namespace.Name, p.framework.Namespace.Name, defaultV2WaitTimeout)
+	}
+}
+
+func (p *ProviderV2) providerConfigName(profile awsAuthProfile) string {
+	return fmt.Sprintf("%s-%s", p.framework.Namespace.Name, profile)
+}

+ 161 - 0
e2e/suites/provider/cases/aws/secretsmanager/push_v2.go

@@ -0,0 +1,161 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	"context"
+	"fmt"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+var _ = Describe("[aws] v2 push secret", Label("aws", "secretsmanager", "v2", "push-secret"), func() {
+	f := framework.New("eso-aws-sm-v2-push")
+	prov := NewProviderV2(f)
+	harness := newAWSClusterProviderPushHarness(f, prov)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+	})
+
+	DescribeTable("push secret",
+		framework.TableFuncWithPushSecret(f, prov, nil),
+		Entry(awsPushSecretImplicitProviderKind(f, prov)),
+		Entry(awsPushSecretRejectsNamespacedRemoteNamespaceOverride(f, prov)),
+		Entry(common.ClusterProviderPushManifestNamespace(f, harness)),
+		Entry(common.ClusterProviderPushProviderNamespace(f, harness)),
+		Entry(common.ClusterProviderPushDeniedByConditions(f, harness)),
+	)
+})
+
+func newAWSClusterProviderPushHarness(f *framework.Framework, prov *ProviderV2) common.ClusterProviderPushHarness {
+	return common.ClusterProviderPushHarness{
+		Prepare: func(_ *framework.TestCase, cfg common.ClusterProviderConfig) *common.ClusterProviderPushRuntime {
+			s := newAWSClusterProviderScenario(f, cfg.Name, cfg.AuthScope, prov.access, prov.backend)
+			clusterProviderName := s.createClusterProvider(cfg.Conditions)
+			frameworkv2.WaitForClusterProviderReady(f, clusterProviderName, defaultV2WaitTimeout)
+
+			return &common.ClusterProviderPushRuntime{
+				ClusterProviderName:    clusterProviderName,
+				DefaultRemoteNamespace: "",
+				WaitForRemoteSecretValue: func(_, name, key, expectedValue string) {
+					s.waitForRemoteSecretValue(name, key, expectedValue)
+				},
+				ExpectNoRemoteSecret: func(_, name string) {
+					s.backend.ExpectSecretAbsent(name)
+				},
+			}
+		},
+	}
+}
+
+func (s *awsClusterProviderScenario) waitForRemoteSecretValue(name, key, expectedValue string) {
+	if key == "" {
+		s.backend.WaitForSecretValue(name, expectedValue)
+		return
+	}
+	s.backend.WaitForSecretValue(name, fmt.Sprintf(`{"%s":"%s"}`, key, expectedValue))
+}
+
+func awsPushSecretImplicitProviderKind(f *framework.Framework, prov *ProviderV2) (string, func(*framework.TestCase)) {
+	return "[aws] should support namespaced Provider refs when push kind is omitted", func(tc *framework.TestCase) {
+		remoteKey := f.MakeRemoteRefKey("aws-v2-push-implicit")
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileStatic)
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "aws-v2-push-implicit-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("value1"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "aws-v2-push-implicit"
+		tc.PushSecret.Spec.DeletionPolicy = esv1alpha1.PushSecretDeletionPolicyDelete
+		tc.PushSecret.Spec.SecretStoreRefs[0].Kind = ""
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: remoteKey,
+					Property:  "value",
+				},
+			},
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			awscommon.WaitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+			prov.backend.WaitForSecretValue(remoteKey, `{"value":"value1"}`)
+
+			Expect(tc.Framework.CRClient.Delete(context.Background(), ps)).To(Succeed())
+			prov.backend.ExpectSecretAbsent(remoteKey)
+		}
+	}
+}
+
+func awsPushSecretRejectsNamespacedRemoteNamespaceOverride(f *framework.Framework, prov *ProviderV2) (string, func(*framework.TestCase)) {
+	return "[aws] should reject remote namespace overrides when pushing through a namespaced Provider", func(tc *framework.TestCase) {
+		remoteKey := f.MakeRemoteRefKey("aws-v2-push-override")
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileStatic)
+		tc.PushSecretSource = &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "aws-v2-push-override-source",
+				Namespace: f.Namespace.Name,
+			},
+			Data: map[string][]byte{
+				"value": []byte("should-not-push"),
+			},
+		}
+		tc.PushSecret.ObjectMeta.Name = "aws-v2-push-override"
+		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
+			Secret: &esv1alpha1.PushSecretSecret{
+				Name: tc.PushSecretSource.Name,
+			},
+		}
+		tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{{
+			Match: esv1alpha1.PushSecretMatch{
+				SecretKey: "value",
+				RemoteRef: esv1alpha1.PushSecretRemoteRef{
+					RemoteKey: remoteKey,
+					Property:  "value",
+				},
+			},
+			Metadata: awscommon.PushSecretMetadataWithRemoteNamespace("ignored-aws-namespace"),
+		}}
+		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+			awscommon.WaitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+			prov.backend.ExpectSecretAbsent(remoteKey)
+			awscommon.ExpectPushSecretEventMessage(tc.Framework, ps.Namespace, ps.Name, `unknown field "remoteNamespace"`)
+		}
+	}
+}

+ 88 - 0
e2e/suites/provider/cases/aws/secretsmanager/secretsmanager_v2_managed.go

@@ -0,0 +1,88 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package aws
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	"github.com/external-secrets/external-secrets-e2e/framework/addon"
+	frameworkv2 "github.com/external-secrets/external-secrets-e2e/framework/v2"
+	awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws"
+	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+)
+
+var _ = Describe("[awsmanaged] v2 IRSA via referenced service account", Label("aws", "secretsmanager", "managed", "v2"), Ordered, func() {
+	f := framework.New("eso-aws-managed-v2-ref")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+		skipIfAWSManagedIRSAEnvMissing(prov.access)
+	})
+
+	DescribeTable("sync secretsmanager secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		framework.Compose(awscommon.WithReferencedIRSA, f, common.SimpleDataSync, useV2ReferencedIRSA(prov)),
+		framework.Compose(awscommon.WithReferencedIRSA, f, common.FindByName, useV2ReferencedIRSA(prov)),
+	)
+})
+
+var _ = Describe("[awsmanaged] v2 with mounted IRSA", Label("aws", "secretsmanager", "managed", "v2"), Ordered, func() {
+	f := framework.New("eso-aws-managed-v2-mounted")
+	prov := NewProviderV2(f)
+
+	BeforeEach(func() {
+		if !framework.IsV2ProviderMode() {
+			Skip("v2 mode only")
+		}
+		skipIfAWSManagedIRSAEnvMissing(prov.access)
+
+		f.Install(addon.NewESO(
+			addon.WithControllerClass(f.BaseName+"-mounted"),
+			addon.WithReleaseName(f.Namespace.Name),
+			addon.WithNamespace(prov.access.SANamespace),
+			addon.WithoutWebhook(),
+			addon.WithoutCertController(),
+			addon.WithV2AWSProvider(),
+			addon.WithV2ProviderServiceAccount("aws", prov.access.SAName),
+		))
+	})
+
+	DescribeTable("sync secretsmanager secrets",
+		framework.TableFuncWithExternalSecret(f, prov),
+		framework.Compose(awscommon.WithMountedIRSA, f, common.SimpleDataSync, useV2MountedIRSA(prov)),
+		framework.Compose(awscommon.WithMountedIRSA, f, common.FindByName, useV2MountedIRSA(prov)),
+	)
+})
+
+func useV2ReferencedIRSA(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProvider(awsAuthProfileReferencedIRSA)
+	}
+}
+
+func useV2MountedIRSA(prov *ProviderV2) func(*framework.TestCase) {
+	return func(tc *framework.TestCase) {
+		tc.Prepare = prov.prepareNamespacedProviderAtAddress(
+			awsAuthProfileMountedIRSA,
+			frameworkv2.ProviderAddressInNamespace("aws", prov.access.SANamespace),
+		)
+	}
+}

+ 131 - 0
e2e/suites/provider/cases/aws/v2_support.go

@@ -0,0 +1,131 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package common
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/gomega"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+const (
+	StaticAccessKeyIDKey     = "kid"
+	StaticSecretAccessKeyKey = "sak"
+	StaticSessionTokenKey    = "st"
+)
+
+type V2ClusterProviderScenario struct {
+	AuthScope            esv1.AuthenticationScope
+	ConfigName           string
+	ConfigNamespace      string
+	NamePrefix           string
+	ProviderNamespace    string
+	ProviderRefNamespace string
+	WorkloadNamespace    string
+}
+
+func CredentialsSecretName(name string) string {
+	return name + "-credentials"
+}
+
+func StaticCredentialsSecretData(kid, sak, st string) map[string]string {
+	return map[string]string{
+		StaticAccessKeyIDKey:     kid,
+		StaticSecretAccessKeyKey: sak,
+		StaticSessionTokenKey:    st,
+	}
+}
+
+func ProviderConfigNamespace(authScope esv1.AuthenticationScope, providerNamespace, workloadNamespace string) string {
+	if authScope == esv1.AuthenticationScopeProviderNamespace {
+		return providerNamespace
+	}
+	return workloadNamespace
+}
+
+func ProviderReferenceNamespace(authScope esv1.AuthenticationScope, providerNamespace string) string {
+	if authScope == esv1.AuthenticationScopeProviderNamespace {
+		return providerNamespace
+	}
+	return ""
+}
+
+func NewV2ClusterProviderScenario(workloadNamespace, prefix string, authScope esv1.AuthenticationScope, createProviderNamespace func(prefix string) string) V2ClusterProviderScenario {
+	providerNamespace := workloadNamespace
+	if authScope == esv1.AuthenticationScopeProviderNamespace && createProviderNamespace != nil {
+		providerNamespace = createProviderNamespace(prefix + "-provider")
+	}
+
+	return V2ClusterProviderScenario{
+		AuthScope:            authScope,
+		ConfigName:           fmt.Sprintf("%s-config", prefix),
+		ConfigNamespace:      ProviderConfigNamespace(authScope, providerNamespace, workloadNamespace),
+		NamePrefix:           fmt.Sprintf("%s-%s", workloadNamespace, prefix),
+		ProviderNamespace:    providerNamespace,
+		ProviderRefNamespace: ProviderReferenceNamespace(authScope, providerNamespace),
+		WorkloadNamespace:    workloadNamespace,
+	}
+}
+
+func (s V2ClusterProviderScenario) ClusterProviderName() string {
+	return fmt.Sprintf("%s-cluster-provider", s.NamePrefix)
+}
+
+func WaitForPushSecretStatus(f *framework.Framework, namespace, name string, status corev1.ConditionStatus) {
+	Eventually(func(g Gomega) {
+		var ps esv1alpha1.PushSecret
+		g.Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: name, Namespace: namespace}, &ps)).To(Succeed())
+		g.Expect(ps.Status.Conditions).NotTo(BeEmpty())
+		for _, condition := range ps.Status.Conditions {
+			if condition.Type == esv1alpha1.PushSecretReady && condition.Status == status {
+				return
+			}
+		}
+		g.Expect(false).To(BeTrue())
+	}, time.Minute, 5*time.Second).Should(Succeed())
+}
+
+func ExpectPushSecretEventMessage(f *framework.Framework, namespace, objectName, expectedMessage string) {
+	Eventually(func() string {
+		events, err := f.KubeClientSet.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{
+			FieldSelector: "involvedObject.name=" + objectName + ",involvedObject.kind=PushSecret",
+		})
+		Expect(err).NotTo(HaveOccurred())
+
+		messages := make([]string, 0, len(events.Items))
+		for _, event := range events.Items {
+			if event.Message != "" {
+				messages = append(messages, event.Message)
+			}
+		}
+		return fmt.Sprintf("%v", messages)
+	}, time.Minute, 5*time.Second).Should(ContainSubstring(expectedMessage))
+}
+
+func PushSecretMetadataWithRemoteNamespace(namespace string) *apiextensionsv1.JSON {
+	return &apiextensionsv1.JSON{Raw: []byte(fmt.Sprintf(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"remoteNamespace":"%s"}}`, namespace))}
+}

+ 46 - 42
e2e/suites/provider/cases/common/fake_provider.go

@@ -24,52 +24,56 @@ import (
 )
 )
 
 
 func FakeProviderSync(f *framework.Framework) (string, func(*framework.TestCase)) {
 func FakeProviderSync(f *framework.Framework) (string, func(*framework.TestCase)) {
-	remoteKey := fmt.Sprintf("fake-sync-%s", f.Namespace.Name)
-	return NamespacedProviderSync(f, NamespacedProviderSyncConfig{
-		Description:        "[fake] should sync a namespaced secret",
-		ExternalSecretName: "fake-sync-es",
-		TargetSecretName:   "fake-sync-target",
-		RemoteKey:          remoteKey,
-		RemoteSecretValue:  `{"value":"fake-sync-value"}`,
-		RemoteProperty:     "value",
-		SecretKey:          "value",
-		ExpectedValue:      "fake-sync-value",
-	})
+	return "[fake] should sync a namespaced secret", func(tc *framework.TestCase) {
+		_, prepare := NamespacedProviderSync(f, NamespacedProviderSyncConfig{
+			Description:        "[fake] should sync a namespaced secret",
+			ExternalSecretName: "fake-sync-es",
+			TargetSecretName:   "fake-sync-target",
+			RemoteKey:          fmt.Sprintf("fake-sync-%s", f.Namespace.Name),
+			RemoteSecretValue:  `{"value":"fake-sync-value"}`,
+			RemoteProperty:     "value",
+			SecretKey:          "value",
+			ExpectedValue:      "fake-sync-value",
+		})
+		prepare(tc)
+	}
 }
 }
 
 
 func FakeProviderRefresh(f *framework.Framework) (string, func(*framework.TestCase)) {
 func FakeProviderRefresh(f *framework.Framework) (string, func(*framework.TestCase)) {
-	remoteKey := fmt.Sprintf("fake-refresh-%s", f.Namespace.Name)
-	return NamespacedProviderRefresh(f, NamespacedProviderRefreshConfig{
-		Description:         "[fake] should refresh after the provider data changes",
-		ExternalSecretName:  "fake-refresh-es",
-		TargetSecretName:    "fake-refresh-target",
-		RemoteKey:           remoteKey,
-		InitialSecretValue:  `{"value":"fake-initial-value"}`,
-		UpdatedSecretValue:  `{"value":"fake-updated-value"}`,
-		RemoteProperty:      "value",
-		SecretKey:           "value",
-		InitialExpectedData: "fake-initial-value",
-		UpdatedExpectedData: "fake-updated-value",
-		RefreshInterval:     10 * time.Second,
-		WaitTimeout:         30 * time.Second,
-	})
+	return "[fake] should refresh after the provider data changes", func(tc *framework.TestCase) {
+		_, prepare := NamespacedProviderRefresh(f, NamespacedProviderRefreshConfig{
+			Description:         "[fake] should refresh after the provider data changes",
+			ExternalSecretName:  "fake-refresh-es",
+			TargetSecretName:    "fake-refresh-target",
+			RemoteKey:           fmt.Sprintf("fake-refresh-%s", f.Namespace.Name),
+			InitialSecretValue:  `{"value":"fake-initial-value"}`,
+			UpdatedSecretValue:  `{"value":"fake-updated-value"}`,
+			RemoteProperty:      "value",
+			SecretKey:           "value",
+			InitialExpectedData: "fake-initial-value",
+			UpdatedExpectedData: "fake-updated-value",
+			RefreshInterval:     10 * time.Second,
+			WaitTimeout:         30 * time.Second,
+		})
+		prepare(tc)
+	}
 }
 }
 
 
 func FakeProviderFind(f *framework.Framework) (string, func(*framework.TestCase)) {
 func FakeProviderFind(f *framework.Framework) (string, func(*framework.TestCase)) {
-	remoteKeyOne := fmt.Sprintf("fake-find-%s-one", f.Namespace.Name)
-	remoteKeyTwo := fmt.Sprintf("fake-find-%s-two", f.Namespace.Name)
-	remoteKeyThree := fmt.Sprintf("fake-find-ignore-%s", f.Namespace.Name)
-	return NamespacedProviderFind(f, NamespacedProviderFindConfig{
-		Description:        "[fake] should sync dataFrom.find matches",
-		ExternalSecretName: "fake-find-es",
-		TargetSecretName:   "fake-find-target",
-		MatchRegExp:        fmt.Sprintf("fake-find-%s-(one|two)", f.Namespace.Name),
-		MatchingSecrets: map[string]string{
-			remoteKeyOne: `{"value":"fake-find-one"}`,
-			remoteKeyTwo: `{"value":"fake-find-two"}`,
-		},
-		IgnoredSecrets: map[string]string{
-			remoteKeyThree: `{"value":"fake-ignore"}`,
-		},
-	})
+	return "[fake] should sync dataFrom.find matches", func(tc *framework.TestCase) {
+		_, prepare := NamespacedProviderFind(f, NamespacedProviderFindConfig{
+			Description:        "[fake] should sync dataFrom.find matches",
+			ExternalSecretName: "fake-find-es",
+			TargetSecretName:   "fake-find-target",
+			MatchRegExp:        fmt.Sprintf("fake-find-%s-(one|two)", f.Namespace.Name),
+			MatchingSecrets: map[string]string{
+				fmt.Sprintf("fake-find-%s-one", f.Namespace.Name): `{"value":"fake-find-one"}`,
+				fmt.Sprintf("fake-find-%s-two", f.Namespace.Name): `{"value":"fake-find-two"}`,
+			},
+			IgnoredSecrets: map[string]string{
+				fmt.Sprintf("fake-find-ignore-%s", f.Namespace.Name): `{"value":"fake-ignore"}`,
+			},
+		})
+		prepare(tc)
+	}
 }
 }

+ 45 - 0
e2e/suites/provider/cases/common/fake_provider_test.go

@@ -0,0 +1,45 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package common
+
+import (
+	"testing"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+)
+
+func TestFakeProviderHelpersDoNotRequireNamespaceAtConstruction(t *testing.T) {
+	t.Parallel()
+
+	f := &framework.Framework{}
+
+	assertDoesNotPanic(t, func() { FakeProviderSync(f) })
+	assertDoesNotPanic(t, func() { FakeProviderRefresh(f) })
+	assertDoesNotPanic(t, func() { FakeProviderFind(f) })
+}
+
+func assertDoesNotPanic(t *testing.T, fn func()) {
+	t.Helper()
+
+	defer func() {
+		if r := recover(); r != nil {
+			t.Fatalf("unexpected panic: %v", r)
+		}
+	}()
+
+	fn()
+}

+ 29 - 0
e2e/suites/provider/cases/common/provider_runtime_test.go

@@ -19,6 +19,12 @@ package common
 import (
 import (
 	"strings"
 	"strings"
 	"testing"
 	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 )
 )
 
 
 func TestClusterProviderExternalSecretRuntimeSupportsAuthLifecycle(t *testing.T) {
 func TestClusterProviderExternalSecretRuntimeSupportsAuthLifecycle(t *testing.T) {
@@ -124,3 +130,26 @@ func TestApplyClusterProviderPushSecretPanicsWithClearMessageWhenRuntimeNil(t *t
 
 
 	applyClusterProviderPushSecret(nil, nil, "remote-secret")
 	applyClusterProviderPushSecret(nil, nil, "remote-secret")
 }
 }
+
+func TestApplyClusterProviderPushSecretUsesSafeObjectNameIndependentOfRemoteKey(t *testing.T) {
+	tc := &framework.TestCase{
+		PushSecret: &esv1alpha1.PushSecret{},
+		PushSecretSource: &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "push-provider-source",
+			},
+		},
+	}
+	runtime := &ClusterProviderPushRuntime{
+		ClusterProviderName: "push-provider-cluster-provider",
+	}
+
+	applyClusterProviderPushSecret(tc, runtime, "/e2e/test-ns/push-provider-remote")
+
+	if got, want := tc.PushSecret.ObjectMeta.Name, "push-provider-source-push-secret"; got != want {
+		t.Fatalf("expected PushSecret name %q, got %q", want, got)
+	}
+	if got, want := tc.PushSecret.Spec.Data[0].Match.RemoteRef.RemoteKey, "/e2e/test-ns/push-provider-remote"; got != want {
+		t.Fatalf("expected remote key %q, got %q", want, got)
+	}
+}

+ 29 - 25
e2e/suites/provider/cases/common/push_secret.go

@@ -232,11 +232,12 @@ func ClusterProviderPushAllowsRemoteNamespaceOverride(f *framework.Framework, ha
 
 
 		var runtime *ClusterProviderPushRuntime
 		var runtime *ClusterProviderPushRuntime
 		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
 		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			remoteSecretName := f.MakeRemoteRefKey("push-remote-override-remote")
 			runtime = harness.Prepare(tc, ClusterProviderConfig{
 			runtime = harness.Prepare(tc, ClusterProviderConfig{
 				Name:      "push-remote-override",
 				Name:      "push-remote-override",
 				AuthScope: esv1.AuthenticationScopeManifestNamespace,
 				AuthScope: esv1.AuthenticationScopeManifestNamespace,
 			})
 			})
-			applyClusterProviderPushSecret(tc, runtime, "push-remote-override-remote")
+			applyClusterProviderPushSecret(tc, runtime, remoteSecretName)
 			if !runtime.SupportsRemoteNamespaceOverrides() {
 			if !runtime.SupportsRemoteNamespaceOverrides() {
 				Skip(fmt.Sprintf("provider %q does not support remote namespace override hooks", runtime.ClusterProviderName))
 				Skip(fmt.Sprintf("provider %q does not support remote namespace override hooks", runtime.ClusterProviderName))
 			}
 			}
@@ -244,9 +245,9 @@ func ClusterProviderPushAllowsRemoteNamespaceOverride(f *framework.Framework, ha
 			tc.PushSecret.Spec.Data[0].Metadata = pushSecretMetadataWithRemoteNamespace(overrideNamespace)
 			tc.PushSecret.Spec.Data[0].Metadata = pushSecretMetadataWithRemoteNamespace(overrideNamespace)
 			tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
 			tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
 				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
 				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
-				runtime.WaitForRemoteSecretValue(overrideNamespace, "push-remote-override-remote", "value", "override-push-value")
+				runtime.WaitForRemoteSecretValue(overrideNamespace, remoteSecretName, "value", "override-push-value")
 				if runtime.SupportsRemoteAbsenceAssertions() {
 				if runtime.SupportsRemoteAbsenceAssertions() {
-					runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, "push-remote-override-remote")
+					runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, remoteSecretName)
 				}
 				}
 			}
 			}
 		}
 		}
@@ -267,6 +268,7 @@ func ClusterProviderPushDeniedByConditions(f *framework.Framework, harness Clust
 
 
 		var runtime *ClusterProviderPushRuntime
 		var runtime *ClusterProviderPushRuntime
 		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
 		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			remoteSecretName := f.MakeRemoteRefKey("push-deny-remote")
 			runtime = harness.Prepare(tc, ClusterProviderConfig{
 			runtime = harness.Prepare(tc, ClusterProviderConfig{
 				Name:      "push-deny",
 				Name:      "push-deny",
 				AuthScope: esv1.AuthenticationScopeManifestNamespace,
 				AuthScope: esv1.AuthenticationScopeManifestNamespace,
@@ -274,14 +276,14 @@ func ClusterProviderPushDeniedByConditions(f *framework.Framework, harness Clust
 					Namespaces: []string{"not-" + f.Namespace.Name},
 					Namespaces: []string{"not-" + f.Namespace.Name},
 				}},
 				}},
 			})
 			})
-			applyClusterProviderPushSecret(tc, runtime, "push-deny-remote")
-		}
-		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
-			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
-			if runtime.SupportsRemoteAbsenceAssertions() {
-				runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, "push-deny-remote")
+			applyClusterProviderPushSecret(tc, runtime, remoteSecretName)
+			tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+				if runtime.SupportsRemoteAbsenceAssertions() {
+					runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, remoteSecretName)
+				}
+				expectEventMessage(tc.Framework, ps.Namespace, ps.Name, "PushSecret", fmt.Sprintf("using ClusterProvider %q is not allowed from namespace %q: denied by spec.conditions", runtime.ClusterProviderName, f.Namespace.Name))
 			}
 			}
-			expectEventMessage(tc.Framework, ps.Namespace, ps.Name, "PushSecret", fmt.Sprintf("using ClusterProvider %q is not allowed from namespace %q: denied by spec.conditions", runtime.ClusterProviderName, f.Namespace.Name))
 		}
 		}
 	}
 	}
 }
 }
@@ -300,15 +302,16 @@ func clusterProviderPushSyncCase(f *framework.Framework, harness ClusterProvider
 
 
 		var runtime *ClusterProviderPushRuntime
 		var runtime *ClusterProviderPushRuntime
 		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
 		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			remoteSecretName := f.MakeRemoteRefKey(fmt.Sprintf("%s-remote", name))
 			runtime = harness.Prepare(tc, ClusterProviderConfig{
 			runtime = harness.Prepare(tc, ClusterProviderConfig{
 				Name:      name,
 				Name:      name,
 				AuthScope: authScope,
 				AuthScope: authScope,
 			})
 			})
-			applyClusterProviderPushSecret(tc, runtime, fmt.Sprintf("%s-remote", name))
-		}
-		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
-			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
-			runtime.WaitForRemoteSecretValue(runtime.DefaultRemoteNamespace, fmt.Sprintf("%s-remote", name), "value", expectedValue)
+			applyClusterProviderPushSecret(tc, runtime, remoteSecretName)
+			tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+				runtime.WaitForRemoteSecretValue(runtime.DefaultRemoteNamespace, remoteSecretName, "value", expectedValue)
+			}
 		}
 		}
 	}
 	}
 }
 }
@@ -327,25 +330,26 @@ func clusterProviderPushRecoveryCase(f *framework.Framework, harness ClusterProv
 
 
 		var runtime *ClusterProviderPushRuntime
 		var runtime *ClusterProviderPushRuntime
 		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
 		tc.Prepare = func(tc *framework.TestCase, _ framework.SecretStoreProvider) {
+			remoteSecretName := f.MakeRemoteRefKey(fmt.Sprintf("%s-remote", name))
 			runtime = harness.Prepare(tc, ClusterProviderConfig{
 			runtime = harness.Prepare(tc, ClusterProviderConfig{
 				Name:      name,
 				Name:      name,
 				AuthScope: authScope,
 				AuthScope: authScope,
 			})
 			})
-			applyClusterProviderPushSecret(tc, runtime, fmt.Sprintf("%s-remote", name))
+			applyClusterProviderPushSecret(tc, runtime, remoteSecretName)
 			if !runtime.SupportsAuthLifecycle() {
 			if !runtime.SupportsAuthLifecycle() {
 				Skip(fmt.Sprintf("provider %q does not support auth lifecycle recovery hooks", runtime.ClusterProviderName))
 				Skip(fmt.Sprintf("provider %q does not support auth lifecycle recovery hooks", runtime.ClusterProviderName))
 			}
 			}
 			tc.PushSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Hour}
 			tc.PushSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Hour}
 			runtime.BreakAuth()
 			runtime.BreakAuth()
-		}
-		tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
-			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
-			if runtime.SupportsRemoteAbsenceAssertions() {
-				runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, fmt.Sprintf("%s-remote", name))
+			tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionFalse)
+				if runtime.SupportsRemoteAbsenceAssertions() {
+					runtime.ExpectNoRemoteSecret(runtime.DefaultRemoteNamespace, remoteSecretName)
+				}
+				runtime.RepairAuth()
+				waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
+				runtime.WaitForRemoteSecretValue(runtime.DefaultRemoteNamespace, remoteSecretName, "value", expectedValue)
 			}
 			}
-			runtime.RepairAuth()
-			waitForPushSecretStatus(tc.Framework, ps.Namespace, ps.Name, corev1.ConditionTrue)
-			runtime.WaitForRemoteSecretValue(runtime.DefaultRemoteNamespace, fmt.Sprintf("%s-remote", name), "value", expectedValue)
 		}
 		}
 	}
 	}
 }
 }
@@ -355,7 +359,7 @@ func applyClusterProviderPushSecret(tc *framework.TestCase, runtime *ClusterProv
 		panic("cluster provider push harness returned nil runtime")
 		panic("cluster provider push harness returned nil runtime")
 	}
 	}
 
 
-	tc.PushSecret.ObjectMeta.Name = fmt.Sprintf("%s-push-secret", remoteSecretName)
+	tc.PushSecret.ObjectMeta.Name = fmt.Sprintf("%s-push-secret", tc.PushSecretSource.Name)
 	tc.PushSecret.Spec.SecretStoreRefs = []esv1alpha1.PushSecretStoreRef{{
 	tc.PushSecret.Spec.SecretStoreRefs = []esv1alpha1.PushSecretStoreRef{{
 		Name:       runtime.ClusterProviderName,
 		Name:       runtime.ClusterProviderName,
 		Kind:       esv1.ClusterProviderKindStr,
 		Kind:       esv1.ClusterProviderKindStr,

+ 82 - 0
e2e/suites/provider/cases/fake/provider_v2_test.go

@@ -0,0 +1,82 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fake
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestUpsertFakeProviderDataReplacesMatchingEntry(t *testing.T) {
+	input := []esv1.FakeProviderData{
+		{Key: "other", Value: "untouched"},
+		{Key: "remote", Value: "old", Version: "v1"},
+	}
+
+	got := upsertFakeProviderData(input, esv1.FakeProviderData{
+		Key:     "remote",
+		Value:   "new",
+		Version: "v1",
+	})
+
+	want := []esv1.FakeProviderData{
+		{Key: "other", Value: "untouched"},
+		{Key: "remote", Value: "new", Version: "v1"},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Fatalf("upsertFakeProviderData mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestRemoveFakeProviderDataRemovesOnlyExactMatch(t *testing.T) {
+	input := []esv1.FakeProviderData{
+		{Key: "remote", Value: "keep-version", Version: "v2"},
+		{Key: "remote", Value: "drop-version", Version: "v1"},
+		{Key: "other", Value: "keep"},
+	}
+
+	got := removeFakeProviderData(input, "remote", "v1")
+
+	want := []esv1.FakeProviderData{
+		{Key: "remote", Value: "keep-version", Version: "v2"},
+		{Key: "other", Value: "keep"},
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Fatalf("removeFakeProviderData mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestProviderReferenceNamespace(t *testing.T) {
+	if got := providerReferenceNamespace(esv1.AuthenticationScopeManifestNamespace, "provider-ns"); got != "" {
+		t.Fatalf("expected empty providerRef namespace for manifest scope, got %q", got)
+	}
+	if got := providerReferenceNamespace(esv1.AuthenticationScopeProviderNamespace, "provider-ns"); got != "provider-ns" {
+		t.Fatalf("expected providerRef namespace for provider scope, got %q", got)
+	}
+}
+
+func TestFakeConfigNamespaceForAuthScope(t *testing.T) {
+	if got := fakeConfigNamespaceForAuthScope(esv1.AuthenticationScopeManifestNamespace, "manifest-ns", "provider-ns"); got != "manifest-ns" {
+		t.Fatalf("expected manifest namespace for manifest scope, got %q", got)
+	}
+	if got := fakeConfigNamespaceForAuthScope(esv1.AuthenticationScopeProviderNamespace, "manifest-ns", "provider-ns"); got != "provider-ns" {
+		t.Fatalf("expected provider namespace for provider scope, got %q", got)
+	}
+}

+ 7 - 0
e2e/suites/provider/cases/fake/regressions.go

@@ -26,6 +26,13 @@ var _ = Describe("[fake] ", Label("fake"), func() {
 	f := framework.New("eso-fake")
 	f := framework.New("eso-fake")
 	prov := NewProvider(f)
 	prov := NewProvider(f)
 
 
+	DescribeTable("namespaced provider",
+		framework.TableFuncWithExternalSecret(f, prov),
+		Entry(common.FakeProviderSync(f)),
+		Entry(common.FakeProviderRefresh(f)),
+		Entry(common.FakeProviderFind(f)),
+	)
+
 	// we use the fake provider to test regressions
 	// we use the fake provider to test regressions
 	DescribeTable("controller regressions",
 	DescribeTable("controller regressions",
 		framework.TableFuncWithExternalSecret(f, prov),
 		framework.TableFuncWithExternalSecret(f, prov),

+ 53 - 26
providers/v2/aws/config.go

@@ -32,36 +32,63 @@ import (
 // This function converts v2 ProviderReference to v1 SecretStoreSpec.
 // This function converts v2 ProviderReference to v1 SecretStoreSpec.
 func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference, string) (*v1.SecretStoreSpec, error) {
 func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference, string) (*v1.SecretStoreSpec, error) {
 	return func(ref *pb.ProviderReference, sourceNamespace string) (*v1.SecretStoreSpec, error) {
 	return func(ref *pb.ProviderReference, sourceNamespace string) (*v1.SecretStoreSpec, error) {
-		if ref.Kind != awsv2alpha1.SecretsManagerKind {
-			return nil, fmt.Errorf("unsupported provider kind: %s", ref.Kind)
-		}
 		namespace := ref.Namespace
 		namespace := ref.Namespace
 		if namespace == "" {
 		if namespace == "" {
 			namespace = sourceNamespace
 			namespace = sourceNamespace
 		}
 		}
-		var awsProvider awsv2alpha1.SecretsManager
-		err := kubeClient.Get(context.Background(), client.ObjectKey{
-			Namespace: namespace,
-			Name:      ref.Name,
-		}, &awsProvider)
-		if err != nil {
-			return nil, err
-		}
-		return &v1.SecretStoreSpec{
-			Provider: &v1.SecretStoreProvider{
-				AWS: &v1.AWSProvider{
-					Service:           v1.AWSServiceSecretsManager,
-					Auth:              awsProvider.Spec.Auth,
-					Role:              awsProvider.Spec.Role,
-					Region:            awsProvider.Spec.Region,
-					AdditionalRoles:   awsProvider.Spec.AdditionalRoles,
-					ExternalID:        awsProvider.Spec.ExternalID,
-					SecretsManager:    awsProvider.Spec.SecretsManager,
-					SessionTags:       awsProvider.Spec.SessionTags,
-					TransitiveTagKeys: awsProvider.Spec.TransitiveTagKeys,
-					Prefix:            awsProvider.Spec.Prefix,
+
+		switch ref.Kind {
+		case awsv2alpha1.SecretsManagerKind:
+			var awsProvider awsv2alpha1.SecretsManager
+			err := kubeClient.Get(context.Background(), client.ObjectKey{
+				Namespace: namespace,
+				Name:      ref.Name,
+			}, &awsProvider)
+			if err != nil {
+				return nil, err
+			}
+			return &v1.SecretStoreSpec{
+				Provider: &v1.SecretStoreProvider{
+					AWS: &v1.AWSProvider{
+						Service:           v1.AWSServiceSecretsManager,
+						Auth:              awsProvider.Spec.Auth,
+						Role:              awsProvider.Spec.Role,
+						Region:            awsProvider.Spec.Region,
+						AdditionalRoles:   awsProvider.Spec.AdditionalRoles,
+						ExternalID:        awsProvider.Spec.ExternalID,
+						SecretsManager:    awsProvider.Spec.SecretsManager,
+						SessionTags:       awsProvider.Spec.SessionTags,
+						TransitiveTagKeys: awsProvider.Spec.TransitiveTagKeys,
+						Prefix:            awsProvider.Spec.Prefix,
+					},
 				},
 				},
-			},
-		}, nil
+			}, nil
+		case awsv2alpha1.ParameterStoreKind:
+			var awsProvider awsv2alpha1.ParameterStore
+			err := kubeClient.Get(context.Background(), client.ObjectKey{
+				Namespace: namespace,
+				Name:      ref.Name,
+			}, &awsProvider)
+			if err != nil {
+				return nil, err
+			}
+			return &v1.SecretStoreSpec{
+				Provider: &v1.SecretStoreProvider{
+					AWS: &v1.AWSProvider{
+						Service:           v1.AWSServiceParameterStore,
+						Auth:              awsProvider.Spec.Auth,
+						Role:              awsProvider.Spec.Role,
+						Region:            awsProvider.Spec.Region,
+						AdditionalRoles:   awsProvider.Spec.AdditionalRoles,
+						ExternalID:        awsProvider.Spec.ExternalID,
+						SessionTags:       awsProvider.Spec.SessionTags,
+						TransitiveTagKeys: awsProvider.Spec.TransitiveTagKeys,
+						Prefix:            awsProvider.Spec.Prefix,
+					},
+				},
+			}, nil
+		default:
+			return nil, fmt.Errorf("unsupported provider kind: %s", ref.Kind)
+		}
 	}
 	}
 }
 }

+ 120 - 0
providers/v2/aws/config_test.go

@@ -0,0 +1,120 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"testing"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
+	pb "github.com/external-secrets/external-secrets/proto/provider"
+)
+
+func TestGetSpecMapperMapsParameterStore(t *testing.T) {
+	t.Parallel()
+
+	scheme := runtime.NewScheme()
+	if err := clientgoscheme.AddToScheme(scheme); err != nil {
+		t.Fatalf("AddToScheme() error = %v", err)
+	}
+	if err := awsv2alpha1.AddToScheme(scheme); err != nil {
+		t.Fatalf("AddToScheme() error = %v", err)
+	}
+
+	kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&awsv2alpha1.ParameterStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "ps-config",
+			Namespace: "provider-ns",
+		},
+		Spec: awsv2alpha1.ParameterStoreSpec{
+			Region: "eu-central-1",
+			Role:   "arn:aws:iam::123456789012:role/eso-ssm",
+			Prefix: "/team-a/",
+			ExternalID: "ext-id",
+		},
+	}).Build()
+
+	spec, err := GetSpecMapper(kubeClient)(&pb.ProviderReference{
+		ApiVersion: awsv2alpha1.GroupVersion.String(),
+		Kind:       awsv2alpha1.ParameterStoreKind,
+		Name:       "ps-config",
+		Namespace:  "provider-ns",
+	}, "workload-ns")
+	if err != nil {
+		t.Fatalf("GetSpecMapper() error = %v", err)
+	}
+	if spec.Provider == nil || spec.Provider.AWS == nil {
+		t.Fatal("expected AWS provider spec to be returned")
+	}
+	if spec.Provider.AWS.Service != esv1.AWSServiceParameterStore {
+		t.Fatalf("expected service %q, got %q", esv1.AWSServiceParameterStore, spec.Provider.AWS.Service)
+	}
+	if spec.Provider.AWS.Region != "eu-central-1" {
+		t.Fatalf("expected region to be preserved, got %q", spec.Provider.AWS.Region)
+	}
+	if spec.Provider.AWS.Role != "arn:aws:iam::123456789012:role/eso-ssm" {
+		t.Fatalf("expected role to be preserved, got %q", spec.Provider.AWS.Role)
+	}
+	if spec.Provider.AWS.Prefix != "/team-a/" {
+		t.Fatalf("expected prefix to be preserved, got %q", spec.Provider.AWS.Prefix)
+	}
+	if spec.Provider.AWS.ExternalID != "ext-id" {
+		t.Fatalf("expected external ID to be preserved, got %q", spec.Provider.AWS.ExternalID)
+	}
+}
+
+func TestGetSpecMapperUsesSourceNamespaceForParameterStore(t *testing.T) {
+	t.Parallel()
+
+	scheme := runtime.NewScheme()
+	if err := clientgoscheme.AddToScheme(scheme); err != nil {
+		t.Fatalf("AddToScheme() error = %v", err)
+	}
+	if err := awsv2alpha1.AddToScheme(scheme); err != nil {
+		t.Fatalf("AddToScheme() error = %v", err)
+	}
+
+	kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&awsv2alpha1.ParameterStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "ps-source-ns",
+			Namespace: "workload-ns",
+		},
+		Spec: awsv2alpha1.ParameterStoreSpec{
+			Region: "eu-west-1",
+		},
+	}).Build()
+
+	spec, err := GetSpecMapper(kubeClient)(&pb.ProviderReference{
+		ApiVersion: awsv2alpha1.GroupVersion.String(),
+		Kind:       awsv2alpha1.ParameterStoreKind,
+		Name:       "ps-source-ns",
+	}, "workload-ns")
+	if err != nil {
+		t.Fatalf("GetSpecMapper() error = %v", err)
+	}
+	if spec.Provider == nil || spec.Provider.AWS == nil {
+		t.Fatal("expected AWS provider spec to be returned")
+	}
+	if spec.Provider.AWS.Service != esv1.AWSServiceParameterStore {
+		t.Fatalf("expected service %q, got %q", esv1.AWSServiceParameterStore, spec.Provider.AWS.Service)
+	}
+}

+ 7 - 4
providers/v2/aws/main.go

@@ -1,11 +1,9 @@
 /*
 /*
-Copyright © The ESO Authors
-
 Licensed under the Apache License, Version 2.0 (the "License");
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 You may obtain a copy of the License at
 
 
-    https://www.apache.org/licenses/LICENSE-2.0
+    http://www.apache.org/licenses/LICENSE-2.0
 
 
 Unless required by applicable law or agreed to in writing, software
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,7 +17,6 @@ limitations under the License.
 package main
 package main
 
 
 import (
 import (
-	"context"
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"log"
 	"log"
@@ -76,12 +73,18 @@ func main() {
 	}
 	}
 	// Setup v1 provider(s)
 	// Setup v1 provider(s)
 	v1Provider0 := store.NewProvider()
 	v1Provider0 := store.NewProvider()
+	v1Provider1 := store.NewProvider()
 	providerMapping := adapterstore.ProviderMapping{
 	providerMapping := adapterstore.ProviderMapping{
 		schema.GroupVersionKind{
 		schema.GroupVersionKind{
 			Group:   "provider.external-secrets.io",
 			Group:   "provider.external-secrets.io",
 			Version: "v2alpha1",
 			Version: "v2alpha1",
 			Kind:    "SecretsManager",
 			Kind:    "SecretsManager",
 		}: v1Provider0,
 		}: v1Provider0,
+		schema.GroupVersionKind{
+			Group:   "provider.external-secrets.io",
+			Version: "v2alpha1",
+			Kind:    "ParameterStore",
+		}: v1Provider1,
 	}
 	}
 
 
 	specMapper := GetSpecMapper(kubeClient)
 	specMapper := GetSpecMapper(kubeClient)

+ 6 - 1
providers/v2/aws/provider.yaml

@@ -10,6 +10,12 @@ stores:
       kind: "SecretsManager"
       kind: "SecretsManager"
     v1Provider: "github.com/external-secrets/external-secrets/providers/v2/aws/store"
     v1Provider: "github.com/external-secrets/external-secrets/providers/v2/aws/store"
     v1ProviderFunc: "NewProvider"
     v1ProviderFunc: "NewProvider"
+  - gvk:
+      group: "provider.external-secrets.io"
+      version: "v2alpha1"
+      kind: "ParameterStore"
+    v1Provider: "github.com/external-secrets/external-secrets/providers/v2/aws/store"
+    v1ProviderFunc: "NewProvider"
 
 
 generators:
 generators:
   - gvk:
   - gvk:
@@ -26,4 +32,3 @@ generators:
     v1GeneratorFunc: "NewSTSGenerator"
     v1GeneratorFunc: "NewSTSGenerator"
 
 
 configPackage: "."
 configPackage: "."
-

+ 1 - 4
providers/v2/fake/main.go

@@ -1,11 +1,9 @@
 /*
 /*
-Copyright © The ESO Authors
-
 Licensed under the Apache License, Version 2.0 (the "License");
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 You may obtain a copy of the License at
 
 
-    https://www.apache.org/licenses/LICENSE-2.0
+    http://www.apache.org/licenses/LICENSE-2.0
 
 
 Unless required by applicable law or agreed to in writing, software
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,7 +17,6 @@ limitations under the License.
 package main
 package main
 
 
 import (
 import (
-	"context"
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"log"
 	"log"

+ 0 - 4
providers/v2/hack/templates/main.go.tmpl

@@ -19,7 +19,6 @@ limitations under the License.
 package main
 package main
 
 
 import (
 import (
-	"context"
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"log"
 	"log"
@@ -37,9 +36,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
 
 
-	{{- if .HasStores}}
-	v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
-	{{- end}}
 	{{- if .HasGenerators}}
 	{{- if .HasGenerators}}
 	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
 	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
 	{{- end}}
 	{{- end}}

+ 1 - 4
providers/v2/kubernetes/main.go

@@ -1,11 +1,9 @@
 /*
 /*
-Copyright © The ESO Authors
-
 Licensed under the Apache License, Version 2.0 (the "License");
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 You may obtain a copy of the License at
 
 
-    https://www.apache.org/licenses/LICENSE-2.0
+    http://www.apache.org/licenses/LICENSE-2.0
 
 
 Unless required by applicable law or agreed to in writing, software
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,7 +17,6 @@ limitations under the License.
 package main
 package main
 
 
 import (
 import (
-	"context"
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"log"
 	"log"