Browse Source

:sparkles: Implement fetch metadata for K8s (#2106)

* Implemente fetch metadata for K8s

Signed-off-by: Sebastián Gómez <sebastiangomezcorrea@gmail.com>
Sebastián Gómez 3 years ago
parent
commit
ee13e61645

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

@@ -54,7 +54,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              |
+| Kubernetes                |      x       |      x       |          x           |            x            |        x         |             |              x              |
 | IBM Cloud Secrets Manager |              |              |                      |                         |        x         |             |                             |
 | Yandex Lockbox            |              |              |                      |                         |        x         |             |                             |
 | Gitlab Variables          |      x       |      x       |                      |                         |        x         |             |                             |

+ 21 - 0
docs/provider/kubernetes.md

@@ -30,6 +30,27 @@ spec:
     remoteRef:
       key: database-credentials
       property: password
+
+  # metadataPolicy to fetch all the labels and annotations in JSON format
+  - secretKey: tags
+    remoteRef:
+      metadataPolicy: Fetch 
+      key: database-credentials
+
+  # metadataPolicy to fetch all the labels in JSON format
+  - secretKey: labels
+    remoteRef:
+      metadataPolicy: Fetch 
+      key: database-credentials
+	  property: labels
+
+  # metadataPolicy to fetch a specific label (dev) from the source secret
+  - secretKey: developer
+    remoteRef:
+      metadataPolicy: Fetch 
+      key: database-credentials
+	  property: labels.dev
+
 ```
 
 #### find by tag & name

+ 148 - 10
pkg/provider/kubernetes/client.go

@@ -15,9 +15,13 @@ package kubernetes
 
 import (
 	"context"
+	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"strings"
 
+	"github.com/tidwall/gjson"
+	v1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	labels "k8s.io/apimachinery/pkg/labels"
@@ -28,27 +32,74 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
+const (
+	metaLabels      = "labels"
+	metaAnnotations = "annotations"
+)
+
 func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	secretMap, err := c.GetSecretMap(ctx, ref)
+	secret, err := c.userSecretClient.Get(ctx, ref.Key, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+	var values map[string][]byte
+	if ref.MetadataPolicy == esv1beta1.ExternalSecretMetadataPolicyFetch {
+		values, err = getSecretMetadata(secret)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		values = secret.Data
+	}
+
+	byteArr, err := getSecretValues(values, ref.MetadataPolicy)
 	if err != nil {
 		return nil, err
 	}
 	if ref.Property != "" {
-		val, ok := secretMap[ref.Property]
-		if !ok {
+		jsonStr := string(byteArr)
+		// We need to search if a given key with a . exists before using gjson operations.
+		idx := strings.Index(ref.Property, ".")
+		if idx > -1 {
+			refProperty := strings.ReplaceAll(ref.Property, ".", "\\.")
+			val := gjson.Get(jsonStr, refProperty)
+			if val.Exists() {
+				return []byte(val.String()), nil
+			}
+		}
+		val := gjson.Get(jsonStr, ref.Property)
+		if !val.Exists() {
 			return nil, fmt.Errorf("property %s does not exist in key %s", ref.Property, ref.Key)
 		}
-		return val, nil
+
+		return []byte(val.String()), nil
 	}
-	strMap := make(map[string]string)
-	for k, v := range secretMap {
-		strMap[k] = string(v)
+
+	return byteArr, nil
+}
+
+func getSecretValues(secretMap map[string][]byte, policy esv1beta1.ExternalSecretMetadataPolicy) ([]byte, error) {
+	var byteArr []byte
+	var err error
+	if policy == esv1beta1.ExternalSecretMetadataPolicyFetch {
+		data := make(map[string]json.RawMessage, len(secretMap))
+		for k, v := range secretMap {
+			data[k] = v
+		}
+		byteArr, err = json.Marshal(data)
+	} else {
+		strMap := make(map[string]string)
+		for k, v := range secretMap {
+			strMap[k] = string(v)
+		}
+		byteArr, err = json.Marshal(strMap)
 	}
-	jsonStr, err := json.Marshal(strMap)
+
 	if err != nil {
 		return nil, fmt.Errorf("unabled to marshal json: %w", err)
 	}
-	return jsonStr, nil
+
+	return byteArr, nil
 }
 
 func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
@@ -69,7 +120,94 @@ func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
 	if err != nil {
 		return nil, err
 	}
-	return secret.Data, nil
+	var tmpMap map[string][]byte
+	if ref.MetadataPolicy == esv1beta1.ExternalSecretMetadataPolicyFetch {
+		tmpMap, err = getSecretMetadata(secret)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		tmpMap = secret.Data
+	}
+
+	if ref.Property != "" {
+		retMap, err := getPropertyMap(ref.Key, ref.Property, tmpMap)
+		if err != nil {
+			return nil, err
+		}
+		return retMap, nil
+	}
+
+	return tmpMap, nil
+}
+
+func getPropertyMap(key, property string, tmpMap map[string][]byte) (map[string][]byte, error) {
+	byteArr, err := json.Marshal(tmpMap)
+	if err != nil {
+		return nil, err
+	}
+	var retMap map[string][]byte
+	jsonStr := string(byteArr)
+	// We need to search if a given key with a . exists before using gjson operations.
+	idx := strings.Index(property, ".")
+	if idx > -1 {
+		refProperty := strings.ReplaceAll(property, ".", "\\.")
+		retMap, err = getMapFromValues(refProperty, jsonStr)
+		if err != nil {
+			return nil, err
+		}
+		if retMap != nil {
+			return retMap, nil
+		}
+	}
+	retMap, err = getMapFromValues(property, jsonStr)
+	if err != nil {
+		return nil, err
+	}
+	if retMap == nil {
+		return nil, fmt.Errorf("property %s does not exist in key %s", property, key)
+	}
+	return retMap, nil
+}
+
+func getMapFromValues(property, jsonStr string) (map[string][]byte, error) {
+	val := gjson.Get(jsonStr, property)
+	if val.Exists() {
+		retMap := make(map[string][]byte)
+		var tmpMap map[string]interface{}
+		decoded, err := base64.StdEncoding.DecodeString(val.String())
+		if err != nil {
+			return nil, err
+		}
+		err = json.Unmarshal(decoded, &tmpMap)
+		if err != nil {
+			return nil, err
+		}
+		for k, v := range tmpMap {
+			b, err := json.Marshal(v)
+			if err != nil {
+				return nil, err
+			}
+			retMap[k] = b
+		}
+		return retMap, nil
+	}
+	return nil, nil
+}
+
+func getSecretMetadata(secret *v1.Secret) (map[string][]byte, error) {
+	var err error
+	tmpMap := make(map[string][]byte)
+	tmpMap[metaLabels], err = json.Marshal(secret.ObjectMeta.Labels)
+	if err != nil {
+		return nil, err
+	}
+	tmpMap[metaAnnotations], err = json.Marshal(secret.ObjectMeta.Annotations)
+	if err != nil {
+		return nil, err
+	}
+
+	return tmpMap, nil
 }
 
 func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {

+ 193 - 0
pkg/provider/kubernetes/client_test.go

@@ -170,6 +170,97 @@ func TestGetSecret(t *testing.T) {
 			},
 			want: []byte(`{"token":"foobar"}`),
 		},
+		{
+			name: "successful case metadata without property",
+			fields: fields{
+				Client: fakeClient{
+					t: t,
+					secretMap: map[string]corev1.Secret{
+						"mysec": {
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{"date": "today"},
+								Labels:      map[string]string{"dev": "seb"},
+							},
+						},
+					},
+				},
+				Namespace: "default",
+			},
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Key:            "mysec",
+			},
+			want: []byte(`{"annotations":{"date":"today"},"labels":{"dev":"seb"}}`),
+		},
+		{
+			name: "successful case metadata with single property",
+			fields: fields{
+				Client: fakeClient{
+					t: t,
+					secretMap: map[string]corev1.Secret{
+						"mysec": {
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{"date": "today"},
+								Labels:      map[string]string{"dev": "seb"},
+							},
+						},
+					},
+				},
+				Namespace: "default",
+			},
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Key:            "mysec",
+				Property:       "labels",
+			},
+			want: []byte(`{"dev":"seb"}`),
+		},
+		{
+			name: "successful case metadata with multiple properties",
+			fields: fields{
+				Client: fakeClient{
+					t: t,
+					secretMap: map[string]corev1.Secret{
+						"mysec": {
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{"date": "today"},
+								Labels:      map[string]string{"dev": "seb"},
+							},
+						},
+					},
+				},
+				Namespace: "default",
+			},
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Key:            "mysec",
+				Property:       "labels.dev",
+			},
+			want: []byte(`seb`),
+		},
+		{
+			name: "error case metadata with wrong property",
+			fields: fields{
+				Client: fakeClient{
+					t: t,
+					secretMap: map[string]corev1.Secret{
+						"mysec": {
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{"date": "today"},
+								Labels:      map[string]string{"dev": "seb"},
+							},
+						},
+					},
+				},
+				Namespace: "default",
+			},
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Key:            "mysec",
+				Property:       "foo",
+			},
+			wantErr: true,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -190,6 +281,108 @@ func TestGetSecret(t *testing.T) {
 	}
 }
 
+func TestGetSecretMap(t *testing.T) {
+	type fields struct {
+		Client       KClient
+		ReviewClient RClient
+		Namespace    string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		ref    esv1beta1.ExternalSecretDataRemoteRef
+
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "successful case metadata without property",
+			fields: fields{
+				Client: fakeClient{
+					t: t,
+					secretMap: map[string]corev1.Secret{
+						"mysec": {
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{"date": "today"},
+								Labels:      map[string]string{"dev": "seb"},
+							},
+						},
+					},
+				},
+				Namespace: "default",
+			},
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Key:            "mysec",
+			},
+			want: map[string][]byte{"annotations": []byte("{\"date\":\"today\"}"), "labels": []byte("{\"dev\":\"seb\"}")},
+		},
+		{
+			name: "successful case metadata with single property",
+			fields: fields{
+				Client: fakeClient{
+					t: t,
+					secretMap: map[string]corev1.Secret{
+						"mysec": {
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{"date": "today"},
+								Labels:      map[string]string{"dev": "seb"},
+							},
+						},
+					},
+				},
+				Namespace: "default",
+			},
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Key:            "mysec",
+				Property:       "labels",
+			},
+			want: map[string][]byte{"dev": []byte("\"seb\"")},
+		},
+		{
+			name: "error case metadata with wrong property",
+			fields: fields{
+				Client: fakeClient{
+					t: t,
+					secretMap: map[string]corev1.Secret{
+						"mysec": {
+							ObjectMeta: metav1.ObjectMeta{
+								Annotations: map[string]string{"date": "today"},
+								Labels:      map[string]string{"dev": "seb"},
+							},
+						},
+					},
+				},
+				Namespace: "default",
+			},
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				MetadataPolicy: esv1beta1.ExternalSecretMetadataPolicyFetch,
+				Key:            "mysec",
+				Property:       "foo",
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := &Client{
+				userSecretClient: tt.fields.Client,
+				userReviewClient: tt.fields.ReviewClient,
+				namespace:        tt.fields.Namespace,
+			}
+			got, err := p.GetSecretMap(context.Background(), tt.ref)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ProviderKubernetes.GetSecretMap() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ProviderKubernetes.GetSecretMap() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
 func TestGetAllSecrets(t *testing.T) {
 	type fields struct {
 		Client       KClient