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

Issue:
GCP regional PushSecrets are not working
**Describe the bug**
A recent change in the external-secrets operator (commit https://github.com/external-secrets/external-secrets/commit/d932fcedb4c04679756d9d6d7f63fb4e8b329dbc) introduced support for GCP regional secrets. When using the PushSecrets feature with a GCP secret store configured for regional secrets, we encounter an error related to the replication configuration.

_Error Message Example_

```yaml
status:
  conditions:
  - lastTransitionTime: "2025-04-14T13:49:13Z"
    message: 'set secret failed: could not write remote ref password to target secretstore
      gcp-store: rpc error: code = InvalidArgument desc = Secret.replication
      should not be provided.'
    reason: Errored
    status: "False"
    type: Ready
  syncedPushSecrets:
    ClusterSecretStore/gcp-store: {}
```

**Background & Analysis:**

- Global Secrets (without location):
Prior to commit https://github.com/external-secrets/external-secrets/commit/d932fcedb4c04679756d9d6d7f63fb4e8b329dbc), the location was used to set a `UserManaged` replication policy. In this case, replication was explicitly configured. When the location was not defined, the replication policy was set to `Automatic`.
(Reference in the GCP Client: [Global Secret Handling](https://github.com/external-secrets/external-secrets/blob/main/pkg/provider/gcp/secretmanager/client.go#L186-L190))

- Regional Secrets (with location):
When now the location is provided (intended to signify regional secrets), the new logic still applies a replication configuration. However, for regional secrets on GCP, replication is implicitly defined by the selected region, and manually setting the replication field is not required and leads to errors.
(Reference in the GCP Client: [Regional Secret Handling](https://github.com/external-secrets/external-secrets/blob/main/pkg/provider/gcp/secretmanager/client.go#L210-L218))

**To Reproduce**

1. Environment Setup:
- Use a Kubernetes cluster with Workload Identity configured.
- Ensure that GCP Secret Manager contains a secret with the corresponding key (e.g., your-secret-key).

2. Install ExternalSecrets v0.15.1:

- Deploy the ExternalSecrets controller using version v0.15.1.


3. Deploy ClusterSecretStore:
Use a configuration similar to the following (with internal names replaced) (the service account has the role roles/secretmanager.admin):
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: <your-clustersecretstore-name>
spec:
  provider:
    gcpsm:
      auth:
        workloadIdentity:
          clusterLocation: <your-cluster-location>
          clusterName: <your-cluster-name>
          serviceAccountRef:
            name: <your-serviceaccount-name>
            namespace: <your-serviceaccount-namespace>
      location: <your-cluster-location>
      projectID: <your-gcp-project-id>
```

4. Deploy following resources:

```yaml
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
  name: test-pwgen
spec:
  allowRepeat: true
  digits: 5
  length: 32
  noUpper: false
  symbolCharacters: -_
  symbols: 5
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: test-password
spec:
  dataFrom:
  - sourceRef:
      generatorRef:
        apiVersion: generators.external-secrets.io/v1alpha1
        kind: Password
        name: test-pwgen
  refreshInterval: 0s
  target:
    creationPolicy: Owner
    deletionPolicy: Retain
    name: test-password-generated
---
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
  name: test-secret
spec:
  data:
  - conversionStrategy: None
    match:
      remoteRef:
        remoteKey: test-secret
      secretKey: password
    metadata:
      apiVersion: kubernetes.external-secrets.io/v1alpha1
      kind: PushSecretMetadata
      spec:
        labels:
          managed-by: external-secrets
  deletionPolicy: None
  refreshInterval: 1h
  secretStoreRefs:
  - kind: ClusterSecretStore
    name: <your-clustersecretstore-name>
  selector:
    secret:
      name: test-password-generated
  updatePolicy: Replace
```

5. Get the error message

```bash
$ kubectl describe pushsecrets.external-secrets.io test-secret

(...)
  Warning  Errored  45s (x5 over 98s)  pushsecret  set secret failed: could not write remote ref password to target secretstore gcp-store: rpc error: code = InvalidArgument desc = Secret.replication should not be provided.
```

**Expected behavior**

- For Global Secrets:
  - Continue to support user-managed replication when no location is specified.

- For Regional Secrets:
  - Avoid setting the replication configuration if a location is defined.

**Additional context**

Synchronising regional secrets works fine, only using `PushSecrets` does not work.


---
Please let me know if further details (logs, configuration excerpts, etc.) are needed. 

Pull request description:
Gc/fix/gcp pushsecret location replication
## Problem Statement
With the introduction of regional Secrets for GCP, PushSecret replication Location configuration stopped working. This is due to the fact we were misusing `store.spec.location` to set it, as opposed to set as a `PushSecretMetadata`. this PR fixes this.

## Related Issue

Fixes #4656

## Proposed Changes
* Adds a new field `replicationLocation` to the `PushSecretMetadata`
## Checklist

- [x] I have read the [contribution guidelines](https://external-secrets.io/latest/contributing/process/#submitting-a-pull-request)
- [x] All commits are signed with `git commit --signoff`
- [x] My changes have reasonable test coverage
- [x] All tests pass with `make test`
- [x] I ensured my PR is ready for review with `make reviewable`


Patch:
diff --git a/docs/provider/google-secrets-manager.md b/docs/provider/google-secrets-manager.md
index f10557714..29b1e7a4b 100644
--- a/docs/provider/google-secrets-manager.md
+++ b/docs/provider/google-secrets-manager.md
@@ -182,7 +182,7 @@ In the case of a `ClusterSecretStore`, you additionally have to define the servi
 
 #### Authorizing the Core Controller Pod
 
-Instead of managing authentication at the `SecretStore` and `ClusterSecretStore` level, you can give the [Core Controller](/api/components/) Pod's service account access to Secret Manager secrets using one of the two WIF approaches described in the previous sections.
+Instead of managing authentication at the `SecretStore` and `ClusterSecretStore` level, you can give the [Core Controller](../api/components/) Pod's service account access to Secret Manager secrets using one of the two WIF approaches described in the previous sections.
 
 To demonstrate this approach, we'll assume you installed ESO using Helm into the `external-secrets` namespace, with `external-secrets` as the release name:
 
@@ -291,18 +291,25 @@ spec:
 
 ### Location and Replication
 
-By default, secrets are automatically replicated across multiple regions. You can specify a single location for your secrets by setting the `location` field:
+By default, secrets are automatically replicated across multiple regions. You can specify a single location for your secrets by setting the `replicationLocation` field:
 
 ```yaml
-apiVersion: external-secrets.io/v1beta1
-kind: SecretStore
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
 metadata:
-  name: gcp-secret-store
+  name: pushsecret-example
 spec:
-  provider:
-    gcpsm:
-      projectID: my-project
-      location: us-east1  # Specify a single location
+  # ... other fields ...
+  data:
+    - match:
+        secretKey: mykey
+        remoteRef:
+          remoteKey: my-secret
+      metadata:
+        apiVersion: kubernetes.external-secrets.io/v1alpha1
+        kind: PushSecretMetadata`
+        spec:
+          replicationLocation: "us-east1"
 ```
 
 ### Customer-Managed Encryption Keys (CMEK)
@@ -346,50 +353,19 @@ spec:
       location: us-east1  # Required when using CMEK
 ```
 
-## Migration Guide: PushSecret Metadata Format (v0.11.x to v0.12.0)
-
-In version 0.12.0, the metadata format for PushSecrets has been standardized to use a structured format. If you're upgrading from v0.11.x, you'll need to update your PushSecret specifications.
+## Regional Secrets
+GCP Secret Manager Regional Secrets are available to be used with both ExternalSecrets and PushSecrets.
 
-### Old Format (v0.11.x)
-```yaml
-apiVersion: external-secrets.io/v1alpha1
-kind: PushSecret
-spec:
-  data:
-    - match:
-        secretKey: mykey
-        remoteRef:
-          remoteKey: my-secret
-      metadata:
-        annotations:
-          key1: "value1"
-        labels:
-          key2: "value2"
-        topics:
-          - "topic1"
-          - "topic2"
-```
+In order to achieve so, add a `location` to your SecretStore definition:
 
-### New Format (v0.12.0+)
 ```yaml
-apiVersion: external-secrets.io/v1alpha1
-kind: PushSecret
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: gcp-secret-store
 spec:
-  data:
-    - match:
-        secretKey: mykey
-        remoteRef:
-          remoteKey: my-secret
-      metadata:
-        apiVersion: kubernetes.external-secrets.io/v1alpha1
-        kind: PushSecretMetadata
-        spec:
-          annotations:
-            key1: "value1"
-          labels:
-            key2: "value2"
-          topics:
-            - "topic1"
-            - "topic2"
-          cmekKeyName: "projects/my-project/locations/us-east1/keyRings/my-keyring/cryptoKeys/my-key"  # Optional: for CMEK
-```
+  provider:
+    gcpsm:
+      projectID: my-project
+      location: us-east1 # uses regional secrets on us-east1
+```
\ No newline at end of file
diff --git a/pkg/provider/gcp/secretmanager/client.go b/pkg/provider/gcp/secretmanager/client.go
index 46edc63fd..bc42c4652 100644
--- a/pkg/provider/gcp/secretmanager/client.go
+++ b/pkg/provider/gcp/secretmanager/client.go
@@ -189,24 +189,21 @@ func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, pushSecr
 			},
 		}
 
-		if c.store.Location != "" {
-			replica := &secretmanagerpb.Replication_UserManaged_Replica{
-				Location: c.store.Location,
+		if pushSecretData.GetMetadata() != nil {
+			replica := &secretmanagerpb.Replication_UserManaged_Replica{}
+			var err error
+			meta, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](pushSecretData.GetMetadata())
+			if err != nil {
+				return fmt.Errorf("failed to parse PushSecret metadata: %w", err)
 			}
-
-			if pushSecretData.GetMetadata() != nil {
-				var err error
-				meta, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](pushSecretData.GetMetadata())
-				if err != nil {
-					return fmt.Errorf("failed to parse PushSecret metadata: %w", err)
-				}
-				if meta != nil && meta.Spec.CMEKKeyName != "" {
-					replica.CustomerManagedEncryption = &secretmanagerpb.CustomerManagedEncryption{
-						KmsKeyName: meta.Spec.CMEKKeyName,
-					}
+			if meta != nil && meta.Spec.ReplicationLocation != "" {
+				replica.Location = meta.Spec.ReplicationLocation
+			}
+			if meta != nil && meta.Spec.CMEKKeyName != "" {
+				replica.CustomerManagedEncryption = &secretmanagerpb.CustomerManagedEncryption{
+					KmsKeyName: meta.Spec.CMEKKeyName,
 				}
 			}
-
 			replication = &secretmanagerpb.Replication{
 				Replication: &secretmanagerpb.Replication_UserManaged_{
 					UserManaged: &secretmanagerpb.Replication_UserManaged{
diff --git a/pkg/provider/gcp/secretmanager/push_secret.go b/pkg/provider/gcp/secretmanager/push_secret.go
index 7c68235df..7fe0c3d3e 100644
--- a/pkg/provider/gcp/secretmanager/push_secret.go
+++ b/pkg/provider/gcp/secretmanager/push_secret.go
@@ -35,11 +35,12 @@ const (
 )
 
 type PushSecretMetadataSpec struct {
-	Annotations map[string]string             `json:"annotations,omitempty"`
-	Labels      map[string]string             `json:"labels,omitempty"`
-	Topics      []string                      `json:"topics,omitempty"`
-	MergePolicy PushSecretMetadataMergePolicy `json:"mergePolicy,omitempty"`
-	CMEKKeyName string                        `json:"cmekKeyName,omitempty"`
+	Annotations         map[string]string             `json:"annotations,omitempty"`
+	Labels              map[string]string             `json:"labels,omitempty"`
+	Topics              []string                      `json:"topics,omitempty"`
+	MergePolicy         PushSecretMetadataMergePolicy `json:"mergePolicy,omitempty"`
+	CMEKKeyName         string                        `json:"cmekKeyName,omitempty"`
+	ReplicationLocation string                        `json:"replicationLocation,omitempty"`
 }
 
 func newPushSecretBuilder(payload []byte, data esv1.PushSecretData) (pushSecretBuilder, error) {


Test patch:
diff --git a/pkg/provider/gcp/secretmanager/client_test.go b/pkg/provider/gcp/secretmanager/client_test.go
index 8ceb63e4b..7f667f69f 100644
--- a/pkg/provider/gcp/secretmanager/client_test.go
+++ b/pkg/provider/gcp/secretmanager/client_test.go
@@ -679,8 +679,17 @@ func TestPushSecret(t *testing.T) {
 		{
 			desc: "successfully pushes a secret with defined region",
 			args: args{
-				store:               &esv1.GCPSMProvider{ProjectID: smtc.projectID, Location: usEast1},
-				mock:                smtc.mockClient,
+				store: &esv1.GCPSMProvider{ProjectID: smtc.projectID},
+				mock:  smtc.mockClient,
+				Metadata: &apiextensionsv1.JSON{
+					Raw: []byte(`{
+						"apiVersion": "kubernetes.external-secrets.io/v1alpha1",
+						"kind": "PushSecretMetadata",
+						"spec": {
+							"replicationLocation": "us-east1"
+						}
+					}`),
+				},
 				GetSecretMockReturn: fakesm.SecretMockReturn{Secret: nil, Err: notFoundError},
 				CreateSecretMockReturn: fakesm.SecretMockReturn{Secret: &secretmanagerpb.Secret{
 					Name: "projects/default/secrets/baz",