From 669351c448ec9fa245eb25ab664dbd56f9d60d91 Mon Sep 17 00:00:00 2001 From: Bernardo Salazar Date: Sat, 23 Dec 2023 11:23:10 +0100 Subject: [PATCH] feat(k8s): extend k8s provider to fetch configmap (#191) The k8s provider now supports fetching values from ConfigMaps. Signed-off-by: Bernardo Salazar --- pkg/providers/k8s/k8s.go | 44 ++++++++---- pkg/providers/k8s/k8s_test.go | 122 +++++++++++++++++++++++++++------- vals_k8s_test.go | 99 ++++++++++++++++++++++++--- 3 files changed, 220 insertions(+), 45 deletions(-) diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index e923f09..86e869d 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -86,14 +86,8 @@ func (p *provider) GetString(path string) (string, error) { if apiVersion != "v1" { return "", fmt.Errorf("Invalid apiVersion %s. Only apiVersion v1 is supported at this time.", apiVersion) } - if kind != "Secret" { - return "", fmt.Errorf("Invalid kind %s. Only kind Secret is supported at this time.", kind) - } - //TODO: - // At this time, only Secret kind with v1 apiVersion version is supported. - // getObject() should be extended to support both ConfigMap and Secrets kind in other apiVersions. - objectData, err := getObject(namespace, name, p.KubeConfigPath, p.KubeContext, context.Background()) + objectData, err := getObject(kind, namespace, name, p.KubeConfigPath, p.KubeContext, context.Background()) if err != nil { return "", fmt.Errorf("Unable to get %s %s/%s: %s", kind, namespace, name, err) } @@ -110,7 +104,7 @@ func (p *provider) GetString(path string) (string, error) { } p.log.Debugf(message) - return string(object), nil + return object, nil } func (p *provider) GetStringMap(path string) (map[string]interface{}, error) { @@ -135,7 +129,7 @@ func buildConfigWithContextFromFlags(context string, kubeconfigPath string) (*re } // Fetch the object from the Kubernetes cluster -func getObject(namespace string, name string, kubeConfigPath string, kubeContext string, ctx context.Context) (map[string][]byte, error) { +func getObject(kind string, namespace string, name string, kubeConfigPath string, kubeContext string, ctx context.Context) (map[string]string, error) { if kubeContext == "" { fmt.Printf("vals-k8s: kubeContext was not provided. Using current context.\n") } @@ -151,10 +145,34 @@ func getObject(namespace string, name string, kubeConfigPath string, kubeContext return nil, fmt.Errorf("Unable to create the Kubernetes client: %s", err) } - object, err := clientset.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("Unable to get the object from Kubernetes: %s", err) + var object map[string]string + + switch kind { + case "Secret": + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("Unable to get the Secret object from Kubernetes: %s", err) + } + object = convertByteMapToStringMap(secret.Data) + case "ConfigMap": + configmap, err := clientset.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("Unable to get the ConfigMap object from Kubernetes: %s", err) + } + object = configmap.Data + default: + return nil, fmt.Errorf("The specified kind is not valid. Valid kinds: Secret, ConfigMap") + } + + return object, nil +} + +func convertByteMapToStringMap(byteMap map[string][]byte) map[string]string { + stringMap := make(map[string]string) + + for key, value := range byteMap { + stringMap[key] = string(value) } - return object.Data, nil + return stringMap } diff --git a/pkg/providers/k8s/k8s_test.go b/pkg/providers/k8s/k8s_test.go index eb1fb5d..43d4fbc 100644 --- a/pkg/providers/k8s/k8s_test.go +++ b/pkg/providers/k8s/k8s_test.go @@ -19,54 +19,106 @@ import ( // kubectl create namespace test-namespace // create a secret: // kubectl create secret generic mysecret -n test-namespace --from-literal=key=p4ssw0rd +// create a configmap: +// kubectl create configmap myconfigmap -n test-namespace --from-literal=key=configValue func Test_getObject(t *testing.T) { homeDir, _ := os.UserHomeDir() testcases := []struct { namespace string + kind string name string kubeConfigPath string - want map[string][]uint8 + want map[string]string wantErr string }{ - // valid kubeConfigPath is specified + // (secret) valid kubeConfigPath is specified { namespace: "test-namespace", + kind: "Secret", name: "mysecret", kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), - want: map[string][]uint8{"key": []uint8("p4ssw0rd")}, + want: map[string]string{"key": "p4ssw0rd"}, wantErr: "", }, - // kubeConfigPath does not exist + // (secret) kubeConfigPath does not exist { namespace: "test-namespace", + kind: "Secret", name: "mysecret", kubeConfigPath: "/tmp/does-not-exist", want: nil, wantErr: "Unable to build Kubeconfig from vals configuration: stat /tmp/does-not-exist: no such file or directory", }, - // namespace does not exist + // (secret) namespace does not exist { namespace: "non-existent-namespace", + kind: "Secret", name: "mysecret", kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), want: nil, - wantErr: "Unable to get the object from Kubernetes: secrets \"mysecret\" not found", + wantErr: "Unable to get the Secret object from Kubernetes: secrets \"mysecret\" not found", }, - // secret does not exist + // (secret) secret does not exist { namespace: "test-namespace", + kind: "Secret", name: "non-existent-secret", kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), want: nil, - wantErr: "Unable to get the object from Kubernetes: secrets \"non-existent-secret\" not found", + wantErr: "Unable to get the Secret object from Kubernetes: secrets \"non-existent-secret\" not found", + }, + // (configmap) valid kubeConfigPath is specified + { + namespace: "test-namespace", + kind: "ConfigMap", + name: "myconfigmap", + kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), + want: map[string]string{"key": "configValue"}, + wantErr: "", + }, + // (configmap) kubeConfigPath does not exist + { + namespace: "test-namespace", + kind: "ConfigMap", + name: "myconfigmap", + kubeConfigPath: "/tmp/does-not-exist", + want: nil, + wantErr: "Unable to build Kubeconfig from vals configuration: stat /tmp/does-not-exist: no such file or directory", + }, + // (configmap) namespace does not exist + { + namespace: "non-existent-namespace", + kind: "ConfigMap", + name: "myconfigmap", + kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), + want: nil, + wantErr: "Unable to get the ConfigMap object from Kubernetes: configmaps \"myconfigmap\" not found", + }, + // (configmap) configmap does not exist + { + namespace: "test-namespace", + kind: "ConfigMap", + name: "non-existent-configmap", + kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), + want: nil, + wantErr: "Unable to get the ConfigMap object from Kubernetes: configmaps \"non-existent-configmap\" not found", + }, + // unsupported kind + { + namespace: "test-namespace", + kind: "UnsupportedKind", + name: "myconfigmap", + kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir), + want: nil, + wantErr: "The specified kind is not valid. Valid kinds: Secret, ConfigMap", }, } for i := range testcases { tc := testcases[i] t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - got, err := getObject(tc.namespace, tc.name, tc.kubeConfigPath, "", context.Background()) + got, err := getObject(tc.kind, tc.namespace, tc.name, tc.kubeConfigPath, "", context.Background()) if err != nil { if err.Error() != tc.wantErr { t.Fatalf("unexpected error: want %q, got %q", tc.wantErr, err.Error()) @@ -205,53 +257,77 @@ func Test_GetString(t *testing.T) { want string wantErr string }{ - // Valid path is specified + // (secret) Valid path is specified { path: "v1/Secret/test-namespace/mysecret/key", want: "p4ssw0rd", wantErr: "", }, - // Invalid path is specified + // (configmap) Valid path is specified + { + path: "v1/ConfigMap/test-namespace/myconfigmap/key", + want: "configValue", + wantErr: "", + }, + // (secret) Invalid path is specified { path: "v1/Secret/test-namespace/mysecret/key/more/path", want: "", wantErr: "Invalid path v1/Secret/test-namespace/mysecret/key/more/path. Path must be in the format ////", }, - // Bad path is specified + // (configmap) Invalid path is specified { - path: "bad/data/path", + path: "v1/ConfigMap/test-namespace/myconfigmap/key/more/path", want: "", - wantErr: "Invalid path bad/data/path. Path must be in the format ////", + wantErr: "Invalid path v1/ConfigMap/test-namespace/myconfigmap/key/more/path. Path must be in the format ////", }, - // Non-existent namespace is specified + // (secret) Non-existent namespace is specified { path: "v1/Secret/badnamespace/secret/key", want: "", - wantErr: "Unable to get Secret badnamespace/secret: Unable to get the object from Kubernetes: secrets \"secret\" not found", + wantErr: "Unable to get Secret badnamespace/secret: Unable to get the Secret object from Kubernetes: secrets \"secret\" not found", }, - // Non-existent secret is specified + // (configmap) Non-existent secret is specified { path: "v1/Secret/test-namespace/badsecret/key", want: "", - wantErr: "Unable to get Secret test-namespace/badsecret: Unable to get the object from Kubernetes: secrets \"badsecret\" not found", + wantErr: "Unable to get Secret test-namespace/badsecret: Unable to get the Secret object from Kubernetes: secrets \"badsecret\" not found", }, - // Non-existent key is requested + // (secret) Non-existent key is requested { path: "v1/Secret/test-namespace/mysecret/non-existent-key", want: "", wantErr: "Key non-existent-key does not exist in test-namespace/mysecret", }, - // Invalid apiVersion specified + // (configmap) Non-existent key is requested + { + path: "v1/ConfigMap/test-namespace/myconfigmap/non-existent-key", + want: "", + wantErr: "Key non-existent-key does not exist in test-namespace/myconfigmap", + }, + // (secret) Invalid apiVersion specified { path: "v2/Secret/test-namespace/mysecret/non-existent-key", want: "", wantErr: "Invalid apiVersion v2. Only apiVersion v1 is supported at this time.", }, - // Invalid kind specified + // (configmap) Invalid apiVersion specified + { + path: "v2/ConfigMap/test-namespace/myconfigmap/non-existent-key", + want: "", + wantErr: "Invalid apiVersion v2. Only apiVersion v1 is supported at this time.", + }, + // Incorrect path is specified + { + path: "bad/data/path", + want: "", + wantErr: "Invalid path bad/data/path. Path must be in the format ////", + }, + // Unsupported kind is specified { - path: "v1/ConfigMap/test-namespace/mysecret/non-existent-key", + path: "v1/UnsupportedKind/test-namespace/myconfigmap/key", want: "", - wantErr: "Invalid kind ConfigMap. Only kind Secret is supported at this time.", + wantErr: "Unable to get UnsupportedKind test-namespace/myconfigmap: The specified kind is not valid. Valid kinds: Secret, ConfigMap", }, } for _, tc := range tests { diff --git a/vals_k8s_test.go b/vals_k8s_test.go index 7bff2e4..677f4a2 100644 --- a/vals_k8s_test.go +++ b/vals_k8s_test.go @@ -16,6 +16,8 @@ func TestValues_k8s(t *testing.T) { // kubectl create namespace test-namespace // create a secret: // kubectl create secret generic mysecret -n test-namespace --from-literal=key=p4ssw0rd + // create a configmap: + // kubectl create configmap myconfigmap -n test-namespace --from-literal=key=configValue type testcase struct { template map[string]interface{} @@ -23,46 +25,125 @@ func TestValues_k8s(t *testing.T) { wantErr string } - apiVersion := "v1" - kind := "Secret" - namespace := "test-namespace" - key := "key" homeDir, _ := os.UserHomeDir() testcases := []testcase{ + // (secret) valid Secret is specified, uses current context { template: map[string]interface{}{ - "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s/%s/%s", apiVersion, kind, namespace, "mysecret", key), + "test_key": "secretref+k8s://v1/Secret/test-namespace/mysecret/key", }, want: map[string]interface{}{ "test_key": "p4ssw0rd", }, wantErr: "", }, + // (secret) valid Secret is specified, with specific kube context { template: map[string]interface{}{ - "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s/%s/%s?kubeContext=minikube", apiVersion, kind, namespace, "mysecret", key), + "test_key": "secretref+k8s://v1/Secret/test-namespace/mysecret/key?kubeContext=minikube", }, want: map[string]interface{}{ "test_key": "p4ssw0rd", }, wantErr: "", }, + // (secret) valid Secret is specified, with specific kube context and kube config path { template: map[string]interface{}{ - "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s/%s/%s?kubeContext=minikube&kubeConfigPath=%s/.kube/config", apiVersion, kind, namespace, "mysecret", key, homeDir), + "test_key": fmt.Sprintf("secretref+k8s://v1/Secret/test-namespace/mysecret/key?kubeContext=minikube&kubeConfigPath=%s/.kube/config", homeDir), }, want: map[string]interface{}{ "test_key": "p4ssw0rd", }, wantErr: "", }, + // (secret) valid Secret is specified, with a kube config path, no specific kube context (uses current) { template: map[string]interface{}{ - "test_key": fmt.Sprintf("secretref+k8s://%s/%s/%s/%s/%s?kubeContext=minikube&kubeConfigPath=%s/.kube/config", "v2", kind, namespace, "mysecret", key, homeDir), + "test_key": fmt.Sprintf("ref+k8s://v1/Secret/test-namespace/mysecret/key?kubeConfigPath=%s/.kube/config", homeDir), + }, + want: map[string]interface{}{ + "test_key": "p4ssw0rd", + }, + wantErr: "", + }, + // (secret) non-existent Secret + { + template: map[string]interface{}{ + "test_key": "ref+k8s://v1/Secret/test-namespace/non-existent-secret/key", + }, + want: nil, + wantErr: "expand k8s://v1/Secret/test-namespace/non-existent-secret/key: Unable to get Secret test-namespace/non-existent-secret: Unable to get the Secret object from Kubernetes: secrets \"non-existent-secret\" not found", + }, + // (configmap) valid ConfigMap is specified, using current context + { + template: map[string]interface{}{ + "test_key": "ref+k8s://v1/ConfigMap/test-namespace/myconfigmap/key", + }, + want: map[string]interface{}{ + "test_key": "configValue", + }, + wantErr: "", + }, + // (configmap) valid Secret is specified, with specific kube context + { + template: map[string]interface{}{ + "test_key": "ref+k8s://v1/ConfigMap/test-namespace/myconfigmap/key?kubeContext=minikube", + }, + want: map[string]interface{}{ + "test_key": "configValue", + }, + wantErr: "", + }, + // (configmap) valid ConfigMap is specified, with specific kube context and kube config path + { + template: map[string]interface{}{ + "test_key": fmt.Sprintf("ref+k8s://v1/ConfigMap/test-namespace/myconfigmap/key?kubeContext=minikube&kubeConfigPath=%s/.kube/config", homeDir), + }, + want: map[string]interface{}{ + "test_key": "configValue", + }, + }, + // (configmap) non-existent ConfigMap + { + template: map[string]interface{}{ + "test_key": "ref+k8s://v1/ConfigMap/test-namespace/non-existent-configmap/key", + }, + want: nil, + wantErr: "expand k8s://v1/ConfigMap/test-namespace/non-existent-configmap/key: Unable to get ConfigMap test-namespace/non-existent-configmap: Unable to get the ConfigMap object from Kubernetes: configmaps \"non-existent-configmap\" not found", + }, + // unsupported kind + { + template: map[string]interface{}{ + "test_key": "ref+k8s://v1/UnsupportedKind/test-namespace/myconfigmap/key", + }, + want: nil, + wantErr: "expand k8s://v1/UnsupportedKind/test-namespace/myconfigmap/key: Unable to get UnsupportedKind test-namespace/myconfigmap: The specified kind is not valid. Valid kinds: Secret, ConfigMap", + }, + // unsupported apiVersion + { + template: map[string]interface{}{ + "test_key": "ref+k8s://v2/ConfigMap/test-namespace/myconfigmap/key", + }, + want: nil, + wantErr: "expand k8s://v2/ConfigMap/test-namespace/myconfigmap/key: Invalid apiVersion v2. Only apiVersion v1 is supported at this time.", + }, + // invalid apiVersion + { + template: map[string]interface{}{ + "test_key": "ref+k8s://invalidApiVersion/ConfigMap/test-namespace/myconfigmap/key", + }, + want: nil, + wantErr: "expand k8s://invalidApiVersion/ConfigMap/test-namespace/myconfigmap/key: Invalid apiVersion invalidApiVersion. Only apiVersion v1 is supported at this time.", + }, + // non-existent namespace + { + template: map[string]interface{}{ + "test_key": "ref+k8s://v1/ConfigMap/non-existent-namespace/myconfigmap/key", }, want: nil, - wantErr: fmt.Sprintf("expand k8s://v2/Secret/test-namespace/mysecret/key?kubeContext=minikube&kubeConfigPath=%s/.kube/config: Invalid apiVersion v2. Only apiVersion v1 is supported at this time.", homeDir), + wantErr: "expand k8s://v1/ConfigMap/non-existent-namespace/myconfigmap/key: Unable to get ConfigMap non-existent-namespace/myconfigmap: Unable to get the ConfigMap object from Kubernetes: configmaps \"myconfigmap\" not found", }, }