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 {
 type PushSecretMatch struct {
 	// Secret Key to be pushed
 	// Secret Key to be pushed
-	SecretKey string `json:"secretKey"`
+	// +optional
+	SecretKey string `json:"secretKey,omitempty"`
 	// Remote Refs to push to providers.
 	// Remote Refs to push to providers.
 	RemoteRef PushSecretRemoteRef `json:"remoteRef"`
 	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
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
+
 package v1beta1
 package v1beta1
 
 
 import apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 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
                           type: string
                       required:
                       required:
                       - remoteRef
                       - remoteRef
-                      - secretKey
                       type: object
                       type: object
                     metadata:
                     metadata:
                       description: Metadata is metadata attached to the secret. The
                       description: Metadata is metadata attached to the secret. The
@@ -225,7 +224,6 @@ spec:
                             type: string
                             type: string
                         required:
                         required:
                         - remoteRef
                         - remoteRef
-                        - secretKey
                         type: object
                         type: object
                       metadata:
                       metadata:
                         description: Metadata is metadata attached to the secret.
                         description: Metadata is metadata attached to the secret.

+ 0 - 2
deploy/crds/bundle.yaml

@@ -4307,7 +4307,6 @@ spec:
                             type: string
                             type: string
                         required:
                         required:
                           - remoteRef
                           - remoteRef
-                          - secretKey
                         type: object
                         type: object
                       metadata:
                       metadata:
                         description: Metadata is metadata attached to the secret. The structure of metadata is provider specific, please look it up in the provider documentation.
                         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
                               type: string
                           required:
                           required:
                             - remoteRef
                             - remoteRef
-                            - secretKey
                           type: object
                           type: object
                         metadata:
                         metadata:
                           description: Metadata is metadata attached to the secret. The structure of metadata is provider specific, please look it up in the provider documentation.
                           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`. 
 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)
 			return out, fmt.Errorf("could not get secrets client for store %v: %w", store.GetName(), err)
 		}
 		}
 		for _, data := range ps.Spec.Data {
 		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)
 				return out, fmt.Errorf(errSetSecretFailed, data.Match.SecretKey, store.GetName(), err)
 			}
 			}
+
 			out[storeKey][statusRef(data)] = data
 			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/find"
 	"github.com/external-secrets/external-secrets/pkg/metrics"
 	"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/provider/aws/util"
+	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 )
 
 
 // https://github.com/external-secrets/external-secrets/issues/644
 // 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"
 	parameterType := "String"
 	overwrite := true
 	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)
 	stringValue := string(value)
 	secretName := data.GetRemoteKey()
 	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 {
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
 		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{
 			ps := ParameterStore{
 				client: &tc.args.client,
 				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 {
 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()
 	secretName := psd.GetRemoteKey()
 	value := secret.Data[psd.GetSecretKey()]
 	value := secret.Data[psd.GetSecretKey()]
 	managedBy := managedBy
 	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.
 // PushSecret stores secrets into a Key vault instance.
 func (a *Azure) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
 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()})
 	objectType, secretName := getObjType(esv1beta1.ExternalSecretDataRemoteRef{Key: data.GetRemoteKey()})
 	value := secret.Data[data.GetSecretKey()]
 	value := secret.Data[data.GetSecretKey()]
 	switch objectType {
 	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.
 // PushSecret pushes a kubernetes secret key into gcp provider Secret.
 func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, pushSecretData esv1beta1.PushSecretData) error {
 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()]
 	payload := secret.Data[pushSecretData.GetSecretKey()]
 	secretName := fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, pushSecretData.GetRemoteKey())
 	secretName := fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, pushSecretData.GetRemoteKey())
 	gcpSecret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
 	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 {
 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()]
 	value := secret.Data[data.GetSecretKey()]
 	parts, err := c.buildSecretNameAndKey(data)
 	parts, err := c.buildSecretNameAndKey(data)
 	if err != nil {
 	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
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
+
 package kubernetes
 package kubernetes
 
 
 import (
 import (
@@ -19,6 +20,7 @@ import (
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
+	"reflect"
 	"strings"
 	"strings"
 
 
 	"github.com/tidwall/gjson"
 	"github.com/tidwall/gjson"
@@ -52,7 +54,7 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 			m[metaLabels] = secret.Labels
 			m[metaLabels] = secret.Labels
 			m[metaAnnotations] = secret.Annotations
 			m[metaAnnotations] = secret.Annotations
 
 
-			j, err := jsonMarshal(m)
+			j, err := utils.JSONMarshal(m)
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
@@ -63,7 +65,7 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 		for key, val := range secret.Data {
 		for key, val := range secret.Data {
 			m[key] = string(val)
 			m[key] = string(val)
 		}
 		}
-		j, err := jsonMarshal(m)
+		j, err := utils.JSONMarshal(m)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
@@ -73,14 +75,6 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 	return getSecret(secret, ref)
 	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 {
 func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
 	if remoteRef.GetProperty() == "" {
 	if remoteRef.GetProperty() == "" {
 		return fmt.Errorf("requires property in RemoteRef to delete secret value")
 		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 {
 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{})
 	extSecret, getErr := c.userSecretClient.Get(ctx, data.GetRemoteKey(), metav1.GetOptions{})
 	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesGetSecret, getErr)
 	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesGetSecret, getErr)
 	if getErr != nil {
 	if getErr != nil {
@@ -120,17 +114,55 @@ func (c *Client) PushSecret(ctx context.Context, secret *v1.Secret, data esv1bet
 			if secret.Type != "" {
 			if secret.Type != "" {
 				typ = secret.Type
 				typ = secret.Type
 			}
 			}
-			return c.createSecret(ctx, value, typ, data)
+
+			return c.createSecret(ctx, secret, typ, data)
 		}
 		}
 		return getErr
 		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
 		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) {
 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) {
 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 {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -206,7 +238,7 @@ func getMapFromValues(property, jsonStr string) (map[string][]byte, error) {
 			return nil, err
 			return nil, err
 		}
 		}
 		for k, v := range tmpMap {
 		for k, v := range tmpMap {
-			b, err := jsonMarshal(v)
+			b, err := utils.JSONMarshal(v)
 			if err != nil {
 			if err != nil {
 				return nil, err
 				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) {
 func getSecretMetadata(secret *v1.Secret) (map[string][]byte, error) {
 	var err error
 	var err error
 	tmpMap := make(map[string][]byte)
 	tmpMap := make(map[string][]byte)
-	tmpMap[metaLabels], err = jsonMarshal(secret.ObjectMeta.Labels)
+	tmpMap[metaLabels], err = utils.JSONMarshal(secret.ObjectMeta.Labels)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	tmpMap[metaAnnotations], err = jsonMarshal(secret.ObjectMeta.Annotations)
+	tmpMap[metaAnnotations], err = utils.JSONMarshal(secret.ObjectMeta.Annotations)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -255,7 +287,7 @@ func (c *Client) findByTags(ctx context.Context, ref esv1beta1.ExternalSecretFin
 	}
 	}
 	data := make(map[string][]byte)
 	data := make(map[string][]byte)
 	for _, secret := range secrets.Items {
 	for _, secret := range secrets.Items {
-		jsonStr, err := jsonMarshal(convertMap(secret.Data))
+		jsonStr, err := utils.JSONMarshal(convertMap(secret.Data))
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
@@ -279,7 +311,7 @@ func (c *Client) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFin
 		if !matcher.MatchName(secret.Name) {
 		if !matcher.MatchName(secret.Name) {
 			continue
 			continue
 		}
 		}
-		jsonStr, err := jsonMarshal(convertMap(secret.Data))
+		jsonStr, err := utils.JSONMarshal(convertMap(secret.Data))
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
@@ -288,7 +320,7 @@ func (c *Client) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFin
 	return utils.ConvertKeys(ref.ConversionStrategy, data)
 	return utils.ConvertKeys(ref.ConversionStrategy, data)
 }
 }
 
 
-func (c Client) Close(_ context.Context) error {
+func (c *Client) Close(_ context.Context) error {
 	return nil
 	return nil
 }
 }
 
 
@@ -300,15 +332,36 @@ func convertMap(in map[string][]byte) map[string]string {
 	return out
 	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{
 	s := v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      remoteRef.GetRemoteKey(),
 			Name:      remoteRef.GetRemoteKey(),
 			Namespace: c.store.RemoteNamespace,
 			Namespace: c.store.RemoteNamespace,
 		},
 		},
-		Data: map[string][]byte{remoteRef.GetProperty(): value},
+		Data: data,
 		Type: typed,
 		Type: typed,
 	}
 	}
+
 	_, err := c.userSecretClient.Create(ctx, &s, metav1.CreateOptions{})
 	_, err := c.userSecretClient.Create(ctx, &s, metav1.CreateOptions{})
 	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesCreateSecret, err)
 	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesCreateSecret, err)
 	return err
 	return err
@@ -334,15 +387,31 @@ func (c *Client) removeProperty(ctx context.Context, extSecret *v1.Secret, remot
 	return err
 	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 {
 func (c *Client) updateProperty(ctx context.Context, extSecret *v1.Secret, remoteRef esv1beta1.PushSecretRemoteRef, value []byte) error {
 	if extSecret.Data == nil {
 	if extSecret.Data == nil {
 		extSecret.Data = make(map[string][]byte)
 		extSecret.Data = make(map[string][]byte)
 	}
 	}
+
 	// otherwise update remote secret
 	// otherwise update remote secret
 	extSecret.Data[remoteRef.GetProperty()] = value
 	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) {
 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 {
 	if len(path) == 1 {
-		j, err := jsonMarshal(metadata)
+		j, err := utils.JSONMarshal(metadata)
 		if err != nil {
 		if err != nil {
 			return nil, false, err
 			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) {
 func TestPushSecret(t *testing.T) {
 	secretKey := "secret-key"
 	secretKey := "secret-key"
 	type fields struct {
 	type fields struct {
-		Client    KClient
-		PushType  v1.SecretType
-		PushValue string
+		Client KClient
 	}
 	}
 	tests := []struct {
 	tests := []struct {
 		name   string
 		name   string
 		fields fields
 		fields fields
 		data   testingfake.PushSecretData
 		data   testingfake.PushSecretData
+		secret *v1.Secret
 
 
 		wantSecretMap map[string]*v1.Secret
 		wantSecretMap map[string]*v1.Secret
 		wantErr       bool
 		wantErr       bool
 	}{
 	}{
 		{
 		{
-			name: "refuse to work without property",
+			name: "refuse to work without property if secret key is provided",
 			fields: fields{
 			fields: fields{
 				Client: &fakeClient{
 				Client: &fakeClient{
 					t: t,
 					t: t,
@@ -758,12 +757,14 @@ func TestPushSecret(t *testing.T) {
 						},
 						},
 					},
 					},
 				},
 				},
-				PushValue: "bar",
 			},
 			},
 			data: testingfake.PushSecretData{
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
 				SecretKey: secretKey,
 				RemoteKey: "mysec",
 				RemoteKey: "mysec",
 			},
 			},
+			secret: &v1.Secret{
+				Data: map[string][]byte{secretKey: []byte("bar")},
+			},
 			wantErr: true,
 			wantErr: true,
 			wantSecretMap: map[string]*v1.Secret{
 			wantSecretMap: map[string]*v1.Secret{
 				"mysec": {
 				"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",
 			name: "add missing property to existing secret",
 			fields: fields{
 			fields: fields{
 				Client: &fakeClient{
 				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{
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
 				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{
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
 				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{
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
 				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{
 			data: testingfake.PushSecretData{
 				SecretKey: secretKey,
 				SecretKey: secretKey,
@@ -909,13 +1033,9 @@ func TestPushSecret(t *testing.T) {
 				userSecretClient: tt.fields.Client,
 				userSecretClient: tt.fields.Client,
 				store:            &esv1beta1.KubernetesProvider{},
 				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 {
 			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
 				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 {
 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()]
 	value := secret.Data[data.GetSecretKey()]
 	secretName := data.GetRemoteKey()
 	secretName := data.GetRemoteKey()
 	encodedValue := base64.StdEncoding.EncodeToString(value)
 	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 {
 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()]
 	value := secret.Data[data.GetSecretKey()]
 	scwRef, err := decodeScwSecretRef(data.GetRemoteKey())
 	scwRef, err := decodeScwSecretRef(data.GetRemoteKey())
 	if err != nil {
 	if err != nil {

+ 10 - 1
pkg/utils/utils.go

@@ -32,7 +32,7 @@ import (
 
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	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 (
 const (
@@ -40,6 +40,15 @@ const (
 	errExecute = "unable to execute transform template: %s"
 	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.
 // MergeByteMap merges map of byte slices.
 func MergeByteMap(dst, src map[string][]byte) map[string][]byte {
 func MergeByteMap(dst, src map[string][]byte) map[string][]byte {
 	for k, v := range src {
 	for k, v := range src {