Browse Source

Feature/pushsecret kubernetes (#2322)

* Add API changes for push secret to k8s

- Property field similar to ExternalSecret

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* rebase: merge commits

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* New Test cases for existing PushSecret Logic

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* feat: replace property if it exists, but differs

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* feat: restrict usage to having a property always

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* chore: refactor delete to work with property only and cleanup whole secret only if it would be empty otherwise

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* feat: refuse to work without property in spec

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* chore: cleanup code, make it more readable

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* feat: add metric calls for kubernetes

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* chore: reorder test cases

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* feat: make property optional to not break compatibility

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* fix: adapt fake impls to include new method to fix tests

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* feat: change status-ref to include property to allow multi property deletes

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* chore: fix make reviewable complains

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* fix: fix imports from merge conflict

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* chore: adapt latest make reviewable suggestions

Signed-off-by: Stephan Discher <stephan.discher@sap.com>

* docs: update push secret support for k8s provider

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* docs: add Kubernetes PushSecret docs

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

---------

Signed-off-by: Stephan Discher <stephan.discher@sap.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
sdischer-sap 3 years ago
parent
commit
8034079e1d

+ 8 - 0
apis/externalsecrets/v1alpha1/pushsecret_types.go

@@ -73,12 +73,20 @@ type PushSecretSelector struct {
 type PushSecretRemoteRef struct {
 	// Name of the resulting provider secret.
 	RemoteKey string `json:"remoteKey"`
+
+	// Name of the property in the resulting secret
+	// +optional
+	Property string `json:"property,omitempty"`
 }
 
 func (r PushSecretRemoteRef) GetRemoteKey() string {
 	return r.RemoteKey
 }
 
+func (r PushSecretRemoteRef) GetProperty() string {
+	return r.Property
+}
+
 type PushSecretMatch struct {
 	// Secret Key to be pushed
 	SecretKey string `json:"secretKey"`

+ 4 - 0
apis/externalsecrets/v1beta1/fakes/pushremoteref.go

@@ -40,6 +40,10 @@ func (fake *PushRemoteRef) GetRemoteKey() string {
 	return fakeReturns.result1
 }
 
+func (fake *PushRemoteRef) GetProperty() string {
+	return ""
+}
+
 func (fake *PushRemoteRef) GetRemoteKeyCallCount() int {
 	fake.getRemoteKeyMutex.RLock()
 	defer fake.getRemoteKeyMutex.RUnlock()

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

@@ -21,4 +21,5 @@ package v1beta1
 // This interface is to allow using v1alpha1 content in Provider registered in v1beta1.
 type PushRemoteRef interface {
 	GetRemoteKey() string
+	GetProperty() string
 }

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

@@ -51,6 +51,9 @@ spec:
                         remoteRef:
                           description: Remote Refs to push to providers.
                           properties:
+                            property:
+                              description: Name of the property in the resulting secret
+                              type: string
                             remoteKey:
                               description: Name of the resulting provider secret.
                               type: string
@@ -199,6 +202,10 @@ spec:
                           remoteRef:
                             description: Remote Refs to push to providers.
                             properties:
+                              property:
+                                description: Name of the property in the resulting
+                                  secret
+                                type: string
                               remoteKey:
                                 description: Name of the resulting provider secret.
                                 type: string

+ 6 - 0
deploy/crds/bundle.yaml

@@ -3860,6 +3860,9 @@ spec:
                           remoteRef:
                             description: Remote Refs to push to providers.
                             properties:
+                              property:
+                                description: Name of the property in the resulting secret
+                                type: string
                               remoteKey:
                                 description: Name of the resulting provider secret.
                                 type: string
@@ -3985,6 +3988,9 @@ spec:
                             remoteRef:
                               description: Remote Refs to push to providers.
                               properties:
+                                property:
+                                  description: Name of the property in the resulting secret
+                                  type: string
                                 remoteKey:
                                   description: Name of the resulting provider secret.
                                   type: string

+ 1 - 1
docs/introduction/stability-support.md

@@ -62,7 +62,7 @@ The following table show the support for features across different providers.
 | Hashicorp Vault           |      x       |      x       |          x           |            x            |        x         |      x      |              x              |
 | GCP Secret Manager        |      x       |      x       |                      |            x            |        x         |      x      |              x              |
 | Azure Keyvault            |      x       |      x       |          x           |            x            |        x         |      x      |              x              |
-| Kubernetes                |      x       |      x       |          x           |            x            |        x         |             |              x              |
+| Kubernetes                |      x       |      x       |          x           |            x            |        x         |      x      |              x              |
 | IBM Cloud Secrets Manager |              |              |                      |                         |        x         |             |                             |
 | Yandex Lockbox            |              |              |                      |                         |        x         |             |                             |
 | GitLab Variables          |      x       |      x       |                      |                         |        x         |             |                             |

+ 79 - 3
docs/provider/kubernetes.md

@@ -34,20 +34,20 @@ spec:
   # metadataPolicy to fetch all the labels and annotations in JSON format
   - secretKey: tags
     remoteRef:
-      metadataPolicy: Fetch 
+      metadataPolicy: Fetch
       key: database-credentials
 
   # metadataPolicy to fetch all the labels in JSON format
   - secretKey: labels
     remoteRef:
-      metadataPolicy: Fetch 
+      metadataPolicy: Fetch
       key: database-credentials
 	  property: labels
 
   # metadataPolicy to fetch a specific label (dev) from the source secret
   - secretKey: developer
     remoteRef:
-      metadataPolicy: Fetch 
+      metadataPolicy: Fetch
       key: database-credentials
 	  property: labels.dev
 
@@ -232,3 +232,79 @@ spec:
             name: "tls-secret"
             key: "tls.key"
 ```
+
+
+### PushSecret
+
+The PushSecret functionality facilitates the replication of a Kubernetes Secret from one namespace or cluster to another. This feature proves useful in scenarios where you need to share sensitive information, such as credentials or configuration data, across different parts of your infrastructure.
+
+To configure the PushSecret resource, you need to specify the following parameters:
+
+* **Selector**: Specify the selector that identifies the source Secret to be replicated. This selector allows you to target the specific Secret you want to share.
+
+* **SecretKey**: Set the SecretKey parameter to indicate the key within the source Secret that you want to replicate. This ensures that only the relevant information is shared.
+
+* **RemoteRef.Property**: In addition to the above parameters, the Kubernetes provider requires you to set the `remoteRef.property` field. This field specifies the key of the remote Secret resource where the replicated value should be stored.
+
+
+Here's an example:
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 10s
+  secretStoreRefs:
+    - name: k8s-store-remote-ns
+      kind: SecretStore
+  selector:
+    secret:
+      name: pokedex-credentials
+  data:
+    - match:
+        secretKey: best-pokemon
+        remoteRef:
+          remoteKey: remote-best-pokemon
+          property: best-pokemon
+```
+
+To utilize the PushSecret feature effectively, the referenced `SecretStore` requires specific permissions on the target cluster. In particular it requires `create`, `read`, `update` and `delete` permissions on the Secret resource:
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  namespace: remote
+  name: eso-store-push-role
+rules:
+- apiGroups: [""]
+  resources:
+  - secrets
+  verbs:
+  - get
+  - list
+  - watch
+  - create
+  - update
+  - patch
+  - delete
+- apiGroups:
+  - authorization.k8s.io
+  resources:
+  - selfsubjectrulesreviews
+  verbs:
+  - create
+```
+
+#### Implementation Considerations
+
+When utilizing the PushSecret feature and configuring the permissions for the SecretStore, consider the following:
+
+
+* **RBAC Configuration**: Ensure that the Role-Based Access Control (RBAC) configuration for the SecretStore grants the appropriate permissions for creating, reading, and updating resources in the target cluster.
+
+* **Least Privilege Principle**: Adhere to the principle of least privilege when assigning permissions to the SecretStore. Only provide the minimum required permissions to accomplish the desired synchronization between Secrets.
+
+* **Namespace or Cluster Scope**: Depending on your specific requirements, configure the SecretStore to operate at the desired scope, whether it is limited to a specific namespace or encompasses the entire cluster. Consider the security and access control implications of your chosen scope.

+ 2 - 2
hack/api-docs/mkdocs.yml

@@ -87,9 +87,10 @@ nav:
     - AWS Parameter Store: provider/aws-parameter-store.md
     - Azure Key Vault: provider/azure-key-vault.md
     - Google Cloud Secret Manager: provider/google-secrets-manager.md
+    - HashiCorp Vault: provider/hashicorp-vault.md
+    - Kubernetes: provider/kubernetes.md
     - IBM Secrets Manager: provider/ibm-secrets-manager.md
     - Akeyless: provider/akeyless.md
-    - HashiCorp Vault: provider/hashicorp-vault.md
     - Yandex Certificate Manager: provider/yandex-certificate-manager.md
     - Yandex Lockbox: provider/yandex-lockbox.md
     - Alibaba Cloud: provider/alibaba.md
@@ -98,7 +99,6 @@ nav:
     - 1Password Secrets Automation: provider/1password-automation.md
     - Webhook: provider/webhook.md
     - Fake: provider/fake.md
-    - Kubernetes: provider/kubernetes.md
     - senhasegura DevOps Secrets Management (DSM): provider/senhasegura-dsm.md
     - Doppler: provider/doppler.md
     - Keeper Security: provider/keeper-security.md

+ 3 - 0
pkg/constants/constants.go

@@ -62,6 +62,9 @@ const (
 	ProviderKubernetes                         = "Kubernetes"
 	CallKubernetesGetSecret                    = "GetSecret"
 	CallKubernetesListSecrets                  = "ListSecrets"
+	CallKubernetesCreateSecret                 = "CreateSecret"
+	CallKubernetesDeleteSecret                 = "DeleteSecret"
+	CallKubernetesUpdateSecret                 = "UpdateSecret"
 	CallKubernetesCreateSelfSubjectRulesReview = "CreateSelfSubjectRulesReview"
 
 	ProviderIBMSM      = "IBM/SecretsManager"

+ 8 - 1
pkg/controllers/pushsecret/pushsecret_controller.go

@@ -254,7 +254,7 @@ func (r *Reconciler) PushSecretToProviders(ctx context.Context, stores map[esapi
 			if err != nil {
 				return out, fmt.Errorf(errSetSecretFailed, ref.Match.SecretKey, store.GetName(), err)
 			}
-			out[storeKey][ref.Match.RemoteRef.RemoteKey] = ref
+			out[storeKey][statusRef(ref.Match.RemoteRef)] = ref
 		}
 	}
 	return out, nil
@@ -396,3 +396,10 @@ func GetPushSecretCondition(status esapi.PushSecretStatus, condType esapi.PushSe
 	}
 	return nil
 }
+
+func statusRef(ref v1beta1.PushRemoteRef) string {
+	if ref.GetProperty() != "" {
+		return ref.GetRemoteKey() + "/" + ref.GetProperty()
+	}
+	return ref.GetRemoteKey()
+}

+ 4 - 0
pkg/provider/aws/parameterstore/parameterstore_test.go

@@ -55,6 +55,10 @@ func (f fakeRef) GetRemoteKey() string {
 	return f.key
 }
 
+func (f fakeRef) GetProperty() string {
+	return ""
+}
+
 func makeValidParameterStoreTestCase() *parameterstoreTestCase {
 	return &parameterstoreTestCase{
 		fakeClient:     &fakeps.Client{},

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

@@ -373,6 +373,10 @@ func (f fakeRef) GetRemoteKey() string {
 	return f.key
 }
 
+func (f fakeRef) GetProperty() string {
+	return ""
+}
+
 func TestSetSecret(t *testing.T) {
 	managedBy := managedBy
 	notManagedBy := "not-managed-by"

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

@@ -158,6 +158,10 @@ func (f fakeRef) GetRemoteKey() string {
 	return f.key
 }
 
+func (f fakeRef) GetProperty() string {
+	return ""
+}
+
 func TestAzureKeyVaultDeleteSecret(t *testing.T) {
 	unsupportedType := func(smtc *secretManagerTestCase) {
 		smtc.pushRef = fakeRef{

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

@@ -402,6 +402,10 @@ func (f fakeRef) GetRemoteKey() string {
 	return f.key
 }
 
+func (f fakeRef) GetProperty() string {
+	return ""
+}
+
 func TestDeleteSecret(t *testing.T) {
 	fErr := status.Error(codes.NotFound, "failed")
 	notFoundError, _ := apierror.FromError(fErr)

+ 86 - 5
pkg/provider/kubernetes/client.go

@@ -14,6 +14,7 @@ limitations under the License.
 package kubernetes
 
 import (
+	"bytes"
 	"context"
 	"encoding/base64"
 	"encoding/json"
@@ -103,13 +104,51 @@ func getSecretValues(secretMap map[string][]byte, policy esv1beta1.ExternalSecre
 	return byteArr, nil
 }
 
-func (c *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushRemoteRef) error {
-	return fmt.Errorf("not implemented")
+func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
+	if remoteRef.GetProperty() == "" {
+		return fmt.Errorf("requires property in RemoteRef to delete secret value")
+	}
+
+	extSecret, getErr := c.userSecretClient.Get(ctx, remoteRef.GetRemoteKey(), metav1.GetOptions{})
+	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesGetSecret, getErr)
+	if getErr != nil {
+		if apierrors.IsNotFound(getErr) {
+			// return gracefully if no secret exists
+			return nil
+		}
+		return getErr
+	}
+	if _, ok := extSecret.Data[remoteRef.GetProperty()]; !ok {
+		// return gracefully if specified secret does not contain the given property
+		return nil
+	}
+
+	if len(extSecret.Data) > 1 {
+		return c.removeProperty(ctx, extSecret, remoteRef)
+	}
+	return c.fullDelete(ctx, remoteRef.GetRemoteKey())
 }
 
-// Not Implemented PushSecret.
-func (c *Client) PushSecret(_ context.Context, _ []byte, _ esv1beta1.PushRemoteRef) error {
-	return fmt.Errorf("not implemented")
+func (c *Client) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
+	if remoteRef.GetProperty() == "" {
+		return fmt.Errorf("requires property in RemoteRef to push secret value")
+	}
+	extSecret, getErr := c.userSecretClient.Get(ctx, remoteRef.GetRemoteKey(), metav1.GetOptions{})
+	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesGetSecret, getErr)
+	if getErr != nil {
+		// create if it not exists
+		if apierrors.IsNotFound(getErr) {
+			return c.createSecret(ctx, value, remoteRef)
+		}
+		return getErr
+	}
+	// return gracefully if data is already in sync
+	if v, ok := extSecret.Data[remoteRef.GetProperty()]; ok && bytes.Equal(v, value) {
+		return nil
+	}
+
+	// otherwise update remote property
+	return c.updateProperty(ctx, extSecret, remoteRef, value)
 }
 
 func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
@@ -278,3 +317,45 @@ func convertMap(in map[string][]byte) map[string]string {
 	}
 	return out
 }
+
+func (c *Client) createSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
+	s := v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      remoteRef.GetRemoteKey(),
+			Namespace: c.store.RemoteNamespace,
+		},
+		Data: map[string][]byte{remoteRef.GetProperty(): value},
+		Type: "Opaque",
+	}
+	_, err := c.userSecretClient.Create(ctx, &s, metav1.CreateOptions{})
+	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesCreateSecret, err)
+	return err
+}
+
+// fullDelete removes remote secret completely.
+func (c *Client) fullDelete(ctx context.Context, secretName string) error {
+	err := c.userSecretClient.Delete(ctx, secretName, metav1.DeleteOptions{})
+	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesDeleteSecret, err)
+
+	// gracefully return on not found
+	if apierrors.IsNotFound(err) {
+		return nil
+	}
+	return err
+}
+
+// removeProperty removes single data property from remote secret.
+func (c *Client) removeProperty(ctx context.Context, extSecret *v1.Secret, remoteRef esv1beta1.PushRemoteRef) error {
+	delete(extSecret.Data, remoteRef.GetProperty())
+	_, err := c.userSecretClient.Update(ctx, extSecret, metav1.UpdateOptions{})
+	metrics.ObserveAPICall(constants.ProviderKubernetes, constants.CallKubernetesUpdateSecret, err)
+	return err
+}
+
+func (c *Client) updateProperty(ctx context.Context, extSecret *v1.Secret, remoteRef esv1beta1.PushRemoteRef, value []byte) error {
+	// 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
+}

+ 404 - 36
pkg/provider/kubernetes/client_test.go

@@ -19,12 +19,14 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/assert"
-	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 
@@ -34,29 +36,68 @@ const (
 
 type fakeClient struct {
 	t                   *testing.T
-	secretMap           map[string]corev1.Secret
+	secretMap           map[string]*v1.Secret
 	expectedListOptions metav1.ListOptions
 	err                 error
 }
 
-func (fk fakeClient) Get(_ context.Context, name string, _ metav1.GetOptions) (*corev1.Secret, error) {
+func (fk *fakeClient) Get(_ context.Context, name string, _ metav1.GetOptions) (*v1.Secret, error) {
+	if fk.err != nil {
+		return nil, fk.err
+	}
+
 	secret, ok := fk.secretMap[name]
 
 	if !ok {
-		return nil, errors.New(errSomethingWentWrong)
+		return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "Secret"}, "secret")
 	}
-	return &secret, fk.err
+	// return inmutable to simulate external system and avoid accidental side effects
+	sCopy := secret.DeepCopy()
+	// update operation requires to relate names
+	sCopy.Name = name
+	return sCopy, nil
 }
 
-func (fk fakeClient) List(_ context.Context, opts metav1.ListOptions) (*corev1.SecretList, error) {
+func (fk *fakeClient) List(_ context.Context, opts metav1.ListOptions) (*v1.SecretList, error) {
 	assert.Equal(fk.t, fk.expectedListOptions, opts)
-	list := &corev1.SecretList{}
+	list := &v1.SecretList{}
 	for _, v := range fk.secretMap {
-		list.Items = append(list.Items, v)
+		list.Items = append(list.Items, *v)
 	}
 	return list, nil
 }
 
+func (fk *fakeClient) Delete(_ context.Context, name string, _ metav1.DeleteOptions) error {
+	if fk.err != nil {
+		return fk.err
+	}
+
+	_, ok := fk.secretMap[name]
+
+	if !ok {
+		return apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "Secret"}, "secret")
+	}
+	delete(fk.secretMap, name)
+	return nil
+}
+
+func (fk *fakeClient) Create(_ context.Context, secret *v1.Secret, _ metav1.CreateOptions) (*v1.Secret, error) {
+	s := &v1.Secret{
+		Data: secret.Data,
+	}
+	fk.secretMap[secret.Name] = s
+	return s, nil
+}
+
+func (fk *fakeClient) Update(_ context.Context, secret *v1.Secret, _ metav1.UpdateOptions) (*v1.Secret, error) {
+	s, ok := fk.secretMap[secret.Name]
+	if !ok {
+		return nil, errors.New("error while updating secret")
+	}
+	s.Data = secret.Data
+	return s, nil
+}
+
 func TestGetSecret(t *testing.T) {
 	type fields struct {
 		Client       KClient
@@ -74,9 +115,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "secretNotFound",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							Data: map[string][]byte{
 								"token": []byte(`foobar`),
@@ -96,9 +137,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "err GetSecretMap",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t:         t,
-					secretMap: map[string]corev1.Secret{},
+					secretMap: map[string]*v1.Secret{},
 				},
 				Namespace: "default",
 			},
@@ -111,9 +152,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "wrong property",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							Data: map[string][]byte{
 								"token": []byte(`foobar`),
@@ -132,9 +173,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "successful case",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							Data: map[string][]byte{
 								"token": []byte(`foobar`),
@@ -153,9 +194,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "successful case without property",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							Data: map[string][]byte{
 								"token": []byte(`foobar`),
@@ -173,9 +214,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "successful case metadata without property",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{"date": "today"},
@@ -195,9 +236,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "successful case metadata with single property",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{"date": "today"},
@@ -218,9 +259,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "successful case metadata with multiple properties",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{"date": "today"},
@@ -241,9 +282,9 @@ func TestGetSecret(t *testing.T) {
 		{
 			name: "error case metadata with wrong property",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{"date": "today"},
@@ -298,9 +339,9 @@ func TestGetSecretMap(t *testing.T) {
 		{
 			name: "successful case metadata without property",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{"date": "today"},
@@ -320,9 +361,9 @@ func TestGetSecretMap(t *testing.T) {
 		{
 			name: "successful case metadata with single property",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{"date": "today"},
@@ -343,9 +384,9 @@ func TestGetSecretMap(t *testing.T) {
 		{
 			name: "error case metadata with wrong property",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Annotations: map[string]string{"date": "today"},
@@ -403,9 +444,9 @@ func TestGetAllSecrets(t *testing.T) {
 		{
 			name: "use regex",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Name: "mysec",
@@ -439,12 +480,12 @@ func TestGetAllSecrets(t *testing.T) {
 		{
 			name: "use tags/labels",
 			fields: fields{
-				Client: fakeClient{
+				Client: &fakeClient{
 					t: t,
 					expectedListOptions: metav1.ListOptions{
 						LabelSelector: "app=foobar",
 					},
-					secretMap: map[string]corev1.Secret{
+					secretMap: map[string]*v1.Secret{
 						"mysec": {
 							ObjectMeta: metav1.ObjectMeta{
 								Name: "mysec",
@@ -495,3 +536,330 @@ func TestGetAllSecrets(t *testing.T) {
 		})
 	}
 }
+
+func TestDeleteSecret(t *testing.T) {
+	type fields struct {
+		Client KClient
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		ref    esv1beta1.PushRemoteRef
+
+		wantSecretMap map[string]*v1.Secret
+		wantErr       bool
+	}{
+		{
+			name: "refuse to delete without property",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foobar`),
+							},
+						},
+					},
+				},
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+			},
+			wantErr: true,
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token": []byte(`foobar`),
+					},
+				},
+			},
+		},
+		{
+			name: "gracefully ignore not found secret",
+			fields: fields{
+				Client: &fakeClient{
+					t:         t,
+					secretMap: map[string]*v1.Secret{},
+				},
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+				Property:  "token",
+			},
+			wantErr:       false,
+			wantSecretMap: map[string]*v1.Secret{},
+		},
+		{
+			name: "gracefully ignore not found property",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foobar`),
+							},
+						},
+					},
+				},
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+				Property:  "secret",
+			},
+			wantErr: false,
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token": []byte(`foobar`),
+					},
+				},
+			},
+		},
+		{
+			name: "unexpected lookup error",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foobar`),
+							},
+						},
+					},
+					err: errors.New(errSomethingWentWrong),
+				},
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+			},
+			wantErr: true,
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token": []byte(`foobar`),
+					},
+				},
+			},
+		},
+		{
+			name: "delete whole secret if only property should be removed",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foobar`),
+							},
+						},
+					},
+				},
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+				Property:  "token",
+			},
+			wantErr:       false,
+			wantSecretMap: map[string]*v1.Secret{},
+		},
+		{
+			name: "multiple properties, just remove that one",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token":  []byte(`foo`),
+								"secret": []byte(`bar`),
+							},
+						},
+					},
+				},
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+				Property:  "token",
+			},
+			wantErr: false,
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"secret": []byte(`bar`),
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := &Client{
+				userSecretClient: tt.fields.Client,
+			}
+			err := p.DeleteSecret(context.Background(), tt.ref)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ProviderKubernetes.DeleteSecret() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			fClient := tt.fields.Client.(*fakeClient)
+			if diff := cmp.Diff(tt.wantSecretMap, fClient.secretMap); diff != "" {
+				t.Errorf("Unexpected resulting secrets map:  -want, +got :\n%s\n", diff)
+			}
+		})
+	}
+}
+
+func TestPushSecret(t *testing.T) {
+	type fields struct {
+		Client    KClient
+		PushValue string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		ref    esv1beta1.PushRemoteRef
+
+		wantSecretMap map[string]*v1.Secret
+		wantErr       bool
+	}{
+		{
+			name: "refuse to work without property",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foo`),
+							},
+						},
+					},
+				},
+				PushValue: "bar",
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+			},
+			wantErr: true,
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token": []byte(`foo`),
+					},
+				},
+			},
+		},
+		{
+			name: "add missing property to existing secret",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foo`),
+							},
+						},
+					},
+				},
+				PushValue: "bar",
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+				Property:  "secret",
+			},
+			wantErr: false,
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token":  []byte(`foo`),
+						"secret": []byte(`bar`),
+					},
+				},
+			},
+		},
+		{
+			name: "replace existing property in existing secret",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foo`),
+							},
+						},
+					},
+				},
+				PushValue: "bar",
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+				Property:  "token",
+			},
+			wantErr: false,
+			wantSecretMap: map[string]*v1.Secret{
+				"mysec": {
+					Data: map[string][]byte{
+						"token": []byte(`bar`),
+					},
+				},
+			},
+		},
+		{
+			name: "create new secret",
+			fields: fields{
+				Client: &fakeClient{
+					t: t,
+					secretMap: map[string]*v1.Secret{
+						"yoursec": {
+							Data: map[string][]byte{
+								"token": []byte(`foo`),
+							},
+						},
+					},
+				},
+				PushValue: "bar",
+			},
+			ref: v1alpha1.PushSecretRemoteRef{
+				RemoteKey: "mysec",
+				Property:  "secret",
+			},
+			wantErr: false,
+			wantSecretMap: map[string]*v1.Secret{
+				"yoursec": {
+					Data: map[string][]byte{
+						"token": []byte(`foo`),
+					},
+				},
+				"mysec": {
+					Data: map[string][]byte{
+						"secret": []byte(`bar`),
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := &Client{
+				userSecretClient: tt.fields.Client,
+				store:            &esv1beta1.KubernetesProvider{},
+			}
+			err := p.PushSecret(context.Background(), []byte(tt.fields.PushValue), tt.ref)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ProviderKubernetes.DeleteSecret() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			fClient := tt.fields.Client.(*fakeClient)
+			if diff := cmp.Diff(tt.wantSecretMap, fClient.secretMap); diff != "" {
+				t.Errorf("Unexpected resulting secrets map:  -want, +got :\n%s\n", diff)
+			}
+		})
+	}
+}

+ 6 - 3
pkg/provider/kubernetes/provider.go

@@ -19,7 +19,7 @@ import (
 	"fmt"
 
 	authv1 "k8s.io/api/authorization/v1"
-	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
 	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -35,8 +35,11 @@ var _ esv1beta1.SecretsClient = &Client{}
 var _ esv1beta1.Provider = &Provider{}
 
 type KClient interface {
-	Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.Secret, error)
-	List(ctx context.Context, opts metav1.ListOptions) (*corev1.SecretList, error)
+	Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Secret, error)
+	List(ctx context.Context, opts metav1.ListOptions) (*v1.SecretList, error)
+	Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
+	Create(ctx context.Context, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error)
+	Update(ctx context.Context, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error)
 }
 
 type RClient interface {

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

@@ -193,6 +193,10 @@ func (ref pushRemoteRef) GetRemoteKey() string {
 	return string(ref)
 }
 
+func (ref pushRemoteRef) GetProperty() string {
+	return ""
+}
+
 func TestPushSecret(t *testing.T) {
 	t.Run("to new secret", func(t *testing.T) {
 		ctx := context.Background()

+ 4 - 0
pkg/provider/vault/vault_test.go

@@ -1577,6 +1577,10 @@ func (f fakeRef) GetRemoteKey() string {
 	return f.key
 }
 
+func (f fakeRef) GetProperty() string {
+	return ""
+}
+
 func TestSetSecret(t *testing.T) {
 	noPermission := errors.New("no permission")