Browse Source

feat: allow pushing the whole secret to the provider (#2862)

* feat: allow pushing the whole secret to the provider

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

* add documentation about pushing a whole secret

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

* disabling this feature for the rest of the providers for now

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

* added scenario for update with existing property

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

---------

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Gergely Brautigam 2 years ago
parent
commit
3fbe318582

+ 2 - 1
apis/externalsecrets/v1alpha1/pushsecret_types.go

@@ -91,7 +91,8 @@ func (r PushSecretRemoteRef) GetProperty() string {
 
 type PushSecretMatch struct {
 	// Secret Key to be pushed
-	SecretKey string `json:"secretKey"`
+	// +optional
+	SecretKey string `json:"secretKey,omitempty"`
 	// Remote Refs to push to providers.
 	RemoteRef PushSecretRemoteRef `json:"remoteRef"`
 }

+ 1 - 0
apis/externalsecrets/v1beta1/pushsecret_interfaces.go

@@ -11,6 +11,7 @@ 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 v1beta1
 
 import apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"

+ 0 - 2
config/crds/bases/external-secrets.io_pushsecrets.yaml

@@ -65,7 +65,6 @@ spec:
                           type: string
                       required:
                       - remoteRef
-                      - secretKey
                       type: object
                     metadata:
                       description: Metadata is metadata attached to the secret. The
@@ -225,7 +224,6 @@ spec:
                             type: string
                         required:
                         - remoteRef
-                        - secretKey
                         type: object
                       metadata:
                         description: Metadata is metadata attached to the secret.

+ 0 - 2
deploy/crds/bundle.yaml

@@ -4307,7 +4307,6 @@ spec:
                             type: string
                         required:
                           - remoteRef
-                          - secretKey
                         type: object
                       metadata:
                         description: Metadata is metadata attached to the secret. The structure of metadata is provider specific, please look it up in the provider documentation.
@@ -4441,7 +4440,6 @@ spec:
                               type: string
                           required:
                             - remoteRef
-                            - secretKey
                           type: object
                         metadata:
                           description: Metadata is metadata attached to the secret. The structure of metadata is provider specific, please look it up in the provider documentation.

+ 24 - 1
docs/guides/pushsecrets.md

@@ -15,4 +15,27 @@ An interesting use case for `kind=PushSecret` is backing up your current secret
 
 Imagine you have your secrets in GCP and you want to back them up in Azure Key Vault. You would then create a `SecretStore` for each provider, and an `ExternalSecret` to pull the secrets from GCP. This will generetae `kind=Secret` in your cluster that you can use as the source of a `PushSecret` configured with the Azure `SecretStore`. 
 
-![PushSecretBackup](../pictures/diagrams-pushsecret-backup.png)
+![PushSecretBackup](../pictures/diagrams-pushsecret-backup.png)
+
+## Pushing the whole secret
+
+There are two ways to push an entire secret without defining all keys individually.
+
+By leaving off the secret key and remote property options.
+
+```yaml
+{% include 'full-pushsecret-no-key-no-property.yaml' %}
+```
+
+This will result in all keys being pushed as they are into the remote location.
+
+By leaving off the secret key but setting the remote property option.
+
+```yaml
+{% include 'full-pushsecret-no-key-with-property.yaml' %}
+```
+
+This will _marshal_ the entire secret data and push it into this single property as a JSON object.
+
+!!! warning inline end
+    This should _ONLY_ be done if the secret data is marshal-able. Values like, binary data cannot be marshaled and will result in error or invalid secret data.

+ 18 - 0
docs/snippets/full-pushsecret-no-key-no-property.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: pushsecret-example # Customisable
+  namespace: default # Same of the SecretStores
+spec:
+  deletionPolicy: Delete # the provider' secret will be deleted if the PushSecret is deleted
+  refreshInterval: 10s # Refresh interval for which push secret will reconcile
+  secretStoreRefs: # A list of secret stores to push secrets to
+    - name: aws-parameterstore
+      kind: SecretStore
+  selector:
+    secret:
+      name: pokedex-credentials # Source Kubernetes secret to be pushed
+  data:
+    - match:
+        remoteRef:
+          remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)

+ 20 - 0
docs/snippets/full-pushsecret-no-key-with-property.yaml

@@ -0,0 +1,20 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: pushsecret-example # Customisable
+  namespace: default # Same of the SecretStores
+spec:
+  deletionPolicy: Delete # the provider' secret will be deleted if the PushSecret is deleted
+  refreshInterval: 10s # Refresh interval for which push secret will reconcile
+  secretStoreRefs: # A list of secret stores to push secrets to
+    - name: aws-parameterstore
+      kind: SecretStore
+  selector:
+    secret:
+      name: pokedex-credentials # Source Kubernetes secret to be pushed
+  data:
+    - match:
+        secretKey: best-pokemon # Source Kubernetes secret key to be pushed
+        remoteRef:
+          remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)
+          property: single-value-secret # the property to use to push into

+ 6 - 4
pkg/controllers/pushsecret/pushsecret_controller.go

@@ -280,14 +280,16 @@ func (r *Reconciler) PushSecretToProviders(ctx context.Context, stores map[esapi
 			return out, fmt.Errorf("could not get secrets client for store %v: %w", store.GetName(), err)
 		}
 		for _, data := range ps.Spec.Data {
-			if _, ok := secret.Data[data.Match.SecretKey]; !ok {
-				return out, fmt.Errorf("secret key %v does not exist", data.Match.SecretKey)
+			if data.Match.SecretKey != "" {
+				if _, ok := secret.Data[data.Match.SecretKey]; !ok {
+					return out, fmt.Errorf("secret key %v does not exist", data.Match.SecretKey)
+				}
 			}
 
-			err := secretClient.PushSecret(ctx, secret, data)
-			if err != nil {
+			if err := secretClient.PushSecret(ctx, secret, data); err != nil {
 				return out, fmt.Errorf(errSetSecretFailed, data.Match.SecretKey, store.GetName(), err)
 			}
+
 			out[storeKey][statusRef(data)] = data
 		}
 	}

+ 16 - 1
pkg/provider/aws/parameterstore/parameterstore.go

@@ -35,6 +35,7 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/find"
 	"github.com/external-secrets/external-secrets/pkg/metrics"
 	"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
+	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
 // https://github.com/external-secrets/external-secrets/issues/644
@@ -135,7 +136,21 @@ func (pm *ParameterStore) PushSecret(ctx context.Context, secret *corev1.Secret,
 	parameterType := "String"
 	overwrite := true
 
-	value := secret.Data[data.GetSecretKey()]
+	var (
+		value []byte
+		err   error
+	)
+	key := data.GetSecretKey()
+
+	if key == "" {
+		value, err = utils.JSONMarshal(secret.Data)
+		if err != nil {
+			return fmt.Errorf("failed to serialize secret content as JSON: %w", err)
+		}
+	} else {
+		value = secret.Data[key]
+	}
+
 	stringValue := string(value)
 	secretName := data.GetRemoteKey()
 

+ 1 - 1
pkg/provider/aws/parameterstore/parameterstore_test.go

@@ -427,7 +427,7 @@ func TestPushSecret(t *testing.T) {
 
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
-			psd := fake.PushSecretData{SecretKey: "fake-secret-key", RemoteKey: "fake-key"}
+			psd := fake.PushSecretData{SecretKey: fakeSecretKey, RemoteKey: "fake-key"}
 			ps := ParameterStore{
 				client: &tc.args.client,
 			}

+ 4 - 0
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -205,6 +205,10 @@ func (sm *SecretsManager) DeleteSecret(ctx context.Context, remoteRef esv1beta1.
 }
 
 func (sm *SecretsManager) PushSecret(ctx context.Context, secret *corev1.Secret, psd esv1beta1.PushSecretData) error {
+	if psd.GetSecretKey() == "" {
+		return fmt.Errorf("pushing the whole secret is not yet implemented")
+	}
+
 	secretName := psd.GetRemoteKey()
 	value := secret.Data[psd.GetSecretKey()]
 	managedBy := managedBy

+ 4 - 0
pkg/provider/azure/keyvault/keyvault.go

@@ -497,6 +497,10 @@ func (a *Azure) setKeyVaultKey(ctx context.Context, secretName string, value []b
 
 // PushSecret stores secrets into a Key vault instance.
 func (a *Azure) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
+	if data.GetSecretKey() == "" {
+		return fmt.Errorf("pushing the whole secret is not yet implemented")
+	}
+
 	objectType, secretName := getObjType(esv1beta1.ExternalSecretDataRemoteRef{Key: data.GetRemoteKey()})
 	value := secret.Data[data.GetSecretKey()]
 	switch objectType {

+ 4 - 0
pkg/provider/gcp/secretmanager/client.go

@@ -131,6 +131,10 @@ func parseError(err error) error {
 
 // PushSecret pushes a kubernetes secret key into gcp provider Secret.
 func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, pushSecretData esv1beta1.PushSecretData) error {
+	if pushSecretData.GetSecretKey() == "" {
+		return fmt.Errorf("pushing the whole secret is not yet implemented")
+	}
+
 	payload := secret.Data[pushSecretData.GetSecretKey()]
 	secretName := fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, pushSecretData.GetRemoteKey())
 	gcpSecret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{

+ 4 - 0
pkg/provider/keepersecurity/client.go

@@ -162,6 +162,10 @@ func (c *Client) Close(_ context.Context) error {
 }
 
 func (c *Client) PushSecret(_ context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
+	if data.GetSecretKey() == "" {
+		return fmt.Errorf("pushing the whole secret is not yet implemented")
+	}
+
 	value := secret.Data[data.GetSecretKey()]
 	parts, err := c.buildSecretNameAndKey(data)
 	if err != nil {

+ 100 - 31
pkg/provider/kubernetes/client.go

@@ -11,6 +11,7 @@ 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 kubernetes
 
 import (
@@ -19,6 +20,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"reflect"
 	"strings"
 
 	"github.com/tidwall/gjson"
@@ -52,7 +54,7 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 			m[metaLabels] = secret.Labels
 			m[metaAnnotations] = secret.Annotations
 
-			j, err := jsonMarshal(m)
+			j, err := utils.JSONMarshal(m)
 			if err != nil {
 				return nil, err
 			}
@@ -63,7 +65,7 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 		for key, val := range secret.Data {
 			m[key] = string(val)
 		}
-		j, err := jsonMarshal(m)
+		j, err := utils.JSONMarshal(m)
 		if err != nil {
 			return nil, err
 		}
@@ -73,14 +75,6 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 	return getSecret(secret, ref)
 }
 
-func jsonMarshal(t interface{}) ([]byte, error) {
-	buffer := &bytes.Buffer{}
-	encoder := json.NewEncoder(buffer)
-	encoder.SetEscapeHTML(false)
-	err := encoder.Encode(t)
-	return bytes.TrimRight(buffer.Bytes(), "\n"), err
-}
-
 func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
 	if remoteRef.GetProperty() == "" {
 		return fmt.Errorf("requires property in RemoteRef to delete secret value")
@@ -107,10 +101,10 @@ func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecre
 }
 
 func (c *Client) PushSecret(ctx context.Context, secret *v1.Secret, data esv1beta1.PushSecretData) error {
-	if data.GetProperty() == "" {
-		return fmt.Errorf("requires property in RemoteRef to push secret value")
+	if data.GetProperty() == "" && data.GetSecretKey() != "" {
+		return fmt.Errorf("requires property in RemoteRef to push secret value if secret key is defined")
 	}
-	value := secret.Data[data.GetSecretKey()]
+
 	extSecret, getErr := c.userSecretClient.Get(ctx, data.GetRemoteKey(), metav1.GetOptions{})
 	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesGetSecret, getErr)
 	if getErr != nil {
@@ -120,17 +114,55 @@ func (c *Client) PushSecret(ctx context.Context, secret *v1.Secret, data esv1bet
 			if secret.Type != "" {
 				typ = secret.Type
 			}
-			return c.createSecret(ctx, value, typ, data)
+
+			return c.createSecret(ctx, secret, typ, data)
 		}
 		return getErr
 	}
-	// return gracefully if data is already in sync
-	if v, ok := extSecret.Data[data.GetProperty()]; ok && bytes.Equal(v, value) {
+
+	// the whole secret was pushed to the provider
+	if data.GetSecretKey() == "" {
+		if data.GetProperty() != "" {
+			value, err := c.marshalData(secret)
+			if err != nil {
+				return err
+			}
+
+			if v, ok := extSecret.Data[data.GetProperty()]; ok && bytes.Equal(v, value) {
+				return nil
+			}
+
+			return c.updateProperty(ctx, extSecret, data, value)
+		}
+
+		if reflect.DeepEqual(extSecret.Data, secret.Data) {
+			return nil
+		}
+
+		return c.updateMap(ctx, extSecret, secret.Data)
+	}
+
+	// only a single property was pushed
+	if v, ok := extSecret.Data[data.GetProperty()]; ok && bytes.Equal(v, secret.Data[data.GetSecretKey()]) {
 		return nil
 	}
 
-	// otherwise update remote property
-	return c.updateProperty(ctx, extSecret, data, value)
+	return c.updateProperty(ctx, extSecret, data, secret.Data[data.GetSecretKey()])
+}
+
+func (c *Client) marshalData(secret *v1.Secret) ([]byte, error) {
+	values := make(map[string]string)
+	for k, v := range secret.Data {
+		values[k] = string(v)
+	}
+
+	// marshal
+	value, err := utils.JSONMarshal(values)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal secrets into a single property: %w", err)
+	}
+
+	return value, nil
 }
 
 func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
@@ -164,7 +196,7 @@ func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
 }
 
 func getPropertyMap(key, property string, tmpMap map[string][]byte) (map[string][]byte, error) {
-	byteArr, err := jsonMarshal(tmpMap)
+	byteArr, err := utils.JSONMarshal(tmpMap)
 	if err != nil {
 		return nil, err
 	}
@@ -206,7 +238,7 @@ func getMapFromValues(property, jsonStr string) (map[string][]byte, error) {
 			return nil, err
 		}
 		for k, v := range tmpMap {
-			b, err := jsonMarshal(v)
+			b, err := utils.JSONMarshal(v)
 			if err != nil {
 				return nil, err
 			}
@@ -220,11 +252,11 @@ func getMapFromValues(property, jsonStr string) (map[string][]byte, error) {
 func getSecretMetadata(secret *v1.Secret) (map[string][]byte, error) {
 	var err error
 	tmpMap := make(map[string][]byte)
-	tmpMap[metaLabels], err = jsonMarshal(secret.ObjectMeta.Labels)
+	tmpMap[metaLabels], err = utils.JSONMarshal(secret.ObjectMeta.Labels)
 	if err != nil {
 		return nil, err
 	}
-	tmpMap[metaAnnotations], err = jsonMarshal(secret.ObjectMeta.Annotations)
+	tmpMap[metaAnnotations], err = utils.JSONMarshal(secret.ObjectMeta.Annotations)
 	if err != nil {
 		return nil, err
 	}
@@ -255,7 +287,7 @@ func (c *Client) findByTags(ctx context.Context, ref esv1beta1.ExternalSecretFin
 	}
 	data := make(map[string][]byte)
 	for _, secret := range secrets.Items {
-		jsonStr, err := jsonMarshal(convertMap(secret.Data))
+		jsonStr, err := utils.JSONMarshal(convertMap(secret.Data))
 		if err != nil {
 			return nil, err
 		}
@@ -279,7 +311,7 @@ func (c *Client) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFin
 		if !matcher.MatchName(secret.Name) {
 			continue
 		}
-		jsonStr, err := jsonMarshal(convertMap(secret.Data))
+		jsonStr, err := utils.JSONMarshal(convertMap(secret.Data))
 		if err != nil {
 			return nil, err
 		}
@@ -288,7 +320,7 @@ func (c *Client) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFin
 	return utils.ConvertKeys(ref.ConversionStrategy, data)
 }
 
-func (c Client) Close(_ context.Context) error {
+func (c *Client) Close(_ context.Context) error {
 	return nil
 }
 
@@ -300,15 +332,36 @@ func convertMap(in map[string][]byte) map[string]string {
 	return out
 }
 
-func (c *Client) createSecret(ctx context.Context, value []byte, typed v1.SecretType, remoteRef esv1beta1.PushSecretRemoteRef) error {
+func (c *Client) createSecret(ctx context.Context, secret *v1.Secret, typed v1.SecretType, remoteRef esv1beta1.PushSecretData) error {
+	data := make(map[string][]byte)
+
+	if remoteRef.GetProperty() != "" {
+		// set a specific remote key
+		if remoteRef.GetSecretKey() == "" {
+			value, err := c.marshalData(secret)
+			if err != nil {
+				return err
+			}
+
+			data[remoteRef.GetProperty()] = value
+		} else {
+			// push a specific secret key into a specific remote property
+			data[remoteRef.GetProperty()] = secret.Data[remoteRef.GetSecretKey()]
+		}
+	} else {
+		// push the whole secret as is using each key of the secret as a property in the created secret
+		data = secret.Data
+	}
+
 	s := v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      remoteRef.GetRemoteKey(),
 			Namespace: c.store.RemoteNamespace,
 		},
-		Data: map[string][]byte{remoteRef.GetProperty(): value},
+		Data: data,
 		Type: typed,
 	}
+
 	_, err := c.userSecretClient.Create(ctx, &s, metav1.CreateOptions{})
 	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesCreateSecret, err)
 	return err
@@ -334,15 +387,31 @@ func (c *Client) removeProperty(ctx context.Context, extSecret *v1.Secret, remot
 	return err
 }
 
+func (c *Client) updateMap(ctx context.Context, extSecret *v1.Secret, values map[string][]byte) error {
+	// update the existing map with values from the pushed secret but keep existing values in tack.
+	for k, v := range values {
+		extSecret.Data[k] = v
+	}
+
+	return c.updateSecret(ctx, extSecret)
+}
+
 func (c *Client) updateProperty(ctx context.Context, extSecret *v1.Secret, remoteRef esv1beta1.PushSecretRemoteRef, value []byte) error {
 	if extSecret.Data == nil {
 		extSecret.Data = make(map[string][]byte)
 	}
+
 	// otherwise update remote secret
 	extSecret.Data[remoteRef.GetProperty()] = value
-	_, uErr := c.userSecretClient.Update(ctx, extSecret, metav1.UpdateOptions{})
-	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesUpdateSecret, uErr)
-	return uErr
+
+	return c.updateSecret(ctx, extSecret)
+}
+
+func (c *Client) updateSecret(ctx context.Context, extSecret *v1.Secret) error {
+	_, err := c.userSecretClient.Update(ctx, extSecret, metav1.UpdateOptions{})
+	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesUpdateSecret, err)
+
+	return err
 }
 
 func getSecret(secret *v1.Secret, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
@@ -406,7 +475,7 @@ func getFromSecretMetadata(secret *v1.Secret, ref esv1beta1.ExternalSecretDataRe
 	}
 
 	if len(path) == 1 {
-		j, err := jsonMarshal(metadata)
+		j, err := utils.JSONMarshal(metadata)
 		if err != nil {
 			return nil, false, err
 		}

+ 136 - 16
pkg/provider/kubernetes/client_test.go

@@ -733,20 +733,19 @@ func TestDeleteSecret(t *testing.T) {
 func TestPushSecret(t *testing.T) {
 	secretKey := "secret-key"
 	type fields struct {
-		Client    KClient
-		PushType  v1.SecretType
-		PushValue string
+		Client KClient
 	}
 	tests := []struct {
 		name   string
 		fields fields
 		data   testingfake.PushSecretData
+		secret *v1.Secret
 
 		wantSecretMap map[string]*v1.Secret
 		wantErr       bool
 	}{
 		{
-			name: "refuse to work without property",
+			name: "refuse to work without property if secret key is provided",
 			fields: fields{
 				Client: &fakeClient{
 					t: t,
@@ -758,12 +757,14 @@ func TestPushSecret(t *testing.T) {
 						},
 					},
 				},
-				PushValue: "bar",
 			},
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
 				RemoteKey: "mysec",
 			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{secretKey: []byte("bar")},
+			},
 			wantErr: true,
 			wantSecretMap: map[string]*v1.Secret{
 				"mysec": {
@@ -774,6 +775,121 @@ func TestPushSecret(t *testing.T) {
 			},
 		},
 		{
+			name: "push the whole secret if neither remote property or secretKey is defined but keep existing keys",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foo`),
+							},
+						},
+					},
+				},
+			},
+			data: testingfake.PushSecretData{
+				RemoteKey: "mysec",
+			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{"token2": []byte("foo")},
+			},
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token":  []byte(`foo`),
+						"token2": []byte(`foo`),
+					},
+				},
+			},
+		},
+		{
+			name: "push the whole secret while secret exists into a single property",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foo`),
+							},
+						},
+					},
+				},
+			},
+			data: testingfake.PushSecretData{
+				RemoteKey: "mysec",
+				Property:  "token",
+			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{"foo": []byte("bar")},
+			},
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token": []byte(`{"foo":"bar"}`),
+					},
+				},
+			},
+		},
+		{
+			name: "push the whole secret while secret exists but new property is defined should update the secret and keep existing key",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foo`),
+							},
+						},
+					},
+				},
+			},
+			data: testingfake.PushSecretData{
+				RemoteKey: "mysec",
+				Property:  "token2",
+			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{"foo": []byte("bar")},
+			},
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token":  []byte(`foo`),
+						"token2": []byte(`{"foo":"bar"}`),
+					},
+				},
+			},
+		},
+		{
+			name: "push the whole secret as json if remote property is defined but secret key is not given",
+			fields: fields{
+				Client: &fakeClient{
+					t:         t,
+					secretMap: map[string]*v1.Secret{},
+				},
+			},
+			data: testingfake.PushSecretData{
+				RemoteKey: "mysec",
+				Property:  "marshaled",
+			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{
+					"token":  []byte("foo"),
+					"token2": []byte("2"),
+				},
+			},
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"marshaled": []byte(`{"token":"foo","token2":"2"}`),
+					},
+					Type: "Opaque",
+				},
+			},
+		},
+		{
 			name: "add missing property to existing secret",
 			fields: fields{
 				Client: &fakeClient{
@@ -786,7 +902,9 @@ func TestPushSecret(t *testing.T) {
 						},
 					},
 				},
-				PushValue: "bar",
+			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{secretKey: []byte("bar")},
 			},
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
@@ -816,7 +934,9 @@ func TestPushSecret(t *testing.T) {
 						},
 					},
 				},
-				PushValue: "bar",
+			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{secretKey: []byte("bar")},
 			},
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
@@ -845,7 +965,9 @@ func TestPushSecret(t *testing.T) {
 						},
 					},
 				},
-				PushValue: "bar",
+			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{secretKey: []byte("bar")},
 			},
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
@@ -880,8 +1002,10 @@ func TestPushSecret(t *testing.T) {
 						},
 					},
 				},
-				PushType:  v1.SecretTypeDockerConfigJson,
-				PushValue: `{"auths": {"myregistry.localhost": {"username": "{{ .username }}", "password": "{{ .password }}"}}}`,
+			},
+			secret: &v1.Secret{
+				Type: v1.SecretTypeDockerConfigJson,
+				Data: map[string][]byte{secretKey: []byte(`{"auths": {"myregistry.localhost": {"username": "{{ .username }}", "password": "{{ .password }}"}}}`)},
 			},
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
@@ -909,13 +1033,9 @@ func TestPushSecret(t *testing.T) {
 				userSecretClient: tt.fields.Client,
 				store:            &esv1beta1.KubernetesProvider{},
 			}
-			s := &v1.Secret{
-				Type: tt.fields.PushType,
-				Data: map[string][]byte{secretKey: []byte(tt.fields.PushValue)},
-			}
-			err := p.PushSecret(context.Background(), s, tt.data)
+			err := p.PushSecret(context.Background(), tt.secret, tt.data)
 			if (err != nil) != tt.wantErr {
-				t.Errorf("ProviderKubernetes.DeleteSecret() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("ProviderKubernetes error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
 

+ 4 - 0
pkg/provider/oracle/oracle.go

@@ -95,6 +95,10 @@ const (
 )
 
 func (vms *VaultManagementService) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
+	if data.GetSecretKey() == "" {
+		return fmt.Errorf("pushing the whole secret is not yet implemented")
+	}
+
 	value := secret.Data[data.GetSecretKey()]
 	secretName := data.GetRemoteKey()
 	encodedValue := base64.StdEncoding.EncodeToString(value)

+ 4 - 0
pkg/provider/scaleway/client.go

@@ -102,6 +102,10 @@ func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 }
 
 func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
+	if data.GetSecretKey() == "" {
+		return fmt.Errorf("pushing the whole secret is not yet implemented")
+	}
+
 	value := secret.Data[data.GetSecretKey()]
 	scwRef, err := decodeScwSecretRef(data.GetRemoteKey())
 	if err != nil {

+ 10 - 1
pkg/utils/utils.go

@@ -32,7 +32,7 @@ import (
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	template "github.com/external-secrets/external-secrets/pkg/template/v2"
+	"github.com/external-secrets/external-secrets/pkg/template/v2"
 )
 
 const (
@@ -40,6 +40,15 @@ const (
 	errExecute = "unable to execute transform template: %s"
 )
 
+// JSONMarshal takes an interface and returns a new escaped and encoded byte slice.
+func JSONMarshal(t interface{}) ([]byte, error) {
+	buffer := &bytes.Buffer{}
+	encoder := json.NewEncoder(buffer)
+	encoder.SetEscapeHTML(false)
+	err := encoder.Encode(t)
+	return bytes.TrimRight(buffer.Bytes(), "\n"), err
+}
+
 // MergeByteMap merges map of byte slices.
 func MergeByteMap(dst, src map[string][]byte) map[string][]byte {
 	for k, v := range src {