From 8b9153466bc77acd99f9c65e3d14db3780932048 Mon Sep 17 00:00:00 2001 From: mgianluc Date: Tue, 26 Nov 2024 14:05:06 +0100 Subject: [PATCH] (feat) add more template functions - getField: return a field of a resource. Requires the identifier of the resource fetched via TemplateResourceRefs and the field For instance to get the Spec.Replicas of a Deployment. ``` {{ $replicasValue := getField "Deployment" "spec.replicas" }} ``` - removeField: removes a field from a resource Requires the identifier of the resource fetched via TemplateResourceRefs and the field For instance to remove Spec.Replicas of a Deployment ``` {{ removeField "Deployment" "spec.replicas" }} ``` - setField: set a field to a specific value Requires the identifier of the resource fetched via TemplateResourceRefs, the field and new value For instance to update Spec.Replicas of a Deployment ``` {{ setField "Deployment" "spec.replicas" (int64 7) }} ``` - The functions chainRemoveField chainSetField can be used to pipe more modifications one after another. For instance to modify Spec.Replicas, Namespace, ServiceAccount and Paused field ``` {{ $depl := (getResource "Deployment") }} {{ $depl := (chainSetField $depl "spec.replicas" (int64 5) ) }} {{ $depl := (chainSetField $depl "metadata.namespace" .Cluster.metadata.namespace ) }} {{ $depl := (chainSetField $depl "spec.template.spec.serviceAccountName" "default" ) }} {{ $depl := (chainSetField $depl "spec.paused" true ) }} {{ toYaml $depl }} ``` --- controllers/template_instantiation.go | 92 ++++++- controllers/template_instantiation_test.go | 285 +++++++++++++++++++++ go.sum | 6 - 3 files changed, 371 insertions(+), 12 deletions(-) diff --git a/controllers/template_instantiation.go b/controllers/template_instantiation.go index 97ef7a15..5b9831af 100644 --- a/controllers/template_instantiation.go +++ b/controllers/template_instantiation.go @@ -154,7 +154,7 @@ func fecthClusterObjects(ctx context.Context, config *rest.Config, c client.Clie return result, nil } -func instantiateTemplateValues(ctx context.Context, config *rest.Config, c client.Client, +func instantiateTemplateValues(ctx context.Context, config *rest.Config, c client.Client, //nolint: funlen // adding few closures clusterType libsveltosv1beta1.ClusterType, clusterNamespace, clusterName, requestorName, values string, mgmtResources map[string]*unstructured.Unstructured, logger logr.Logger) (string, error) { @@ -182,20 +182,90 @@ func instantiateTemplateValues(ctx context.Context, config *rest.Config, c clien return "" } - var uObject unstructured.Unstructured - uObject.SetUnstructuredContent(u) - uObject.SetManagedFields(nil) - uObject.SetResourceVersion("") - uObject.SetUID("") + uObject := resetFields(u) data, err := yaml.Marshal(uObject.UnstructuredContent()) if err != nil { // Swallow errors inside of a template. + logger.V(logs.LogInfo).Info(fmt.Sprintf("failed with err %v", err)) return "" } return strings.TrimSuffix(string(data), "\n") } + funcMap["getField"] = func(id string, fields string) interface{} { + u, ok := objects.MgmtResources[id] + if !ok { + // Swallow errors inside of a template. + return "" + } + + v, isPresent, err := unstructured.NestedFieldCopy(u, strings.Split(fields, ".")...) + if err != nil || !isPresent { + return "" + } + + return v + } + funcMap["removeField"] = func(id, fields string) string { + u, ok := objects.MgmtResources[id] + if !ok { + // Swallow errors inside of a template. + return "" + } + + unstructured.RemoveNestedField(u, strings.Split(fields, ".")...) + uObject := resetFields(u) + + data, err := yaml.Marshal(uObject) + if err != nil { + // Swallow errors inside of a template. + logger.V(logs.LogInfo).Info(fmt.Sprintf("failed with err %v", err)) + return "" + } + + return strings.TrimSuffix(string(data), "\n") + } + funcMap["setField"] = func(id, fields string, value any) string { + u, ok := objects.MgmtResources[id] + if !ok { + // Swallow errors inside of a template. + return "" + } + + err := unstructured.SetNestedField(u, value, strings.Split(fields, ".")...) + if err != nil { + // Swallow errors inside of a template. + logger.V(logs.LogInfo).Info(fmt.Sprintf("failed with err %v", err)) + return "" + } + + uObject := resetFields(u) + + data, err := yaml.Marshal(uObject.UnstructuredContent()) + if err != nil { + // Swallow errors inside of a template. + logger.V(logs.LogInfo).Info(fmt.Sprintf("failed with err %v", err)) + return "" + } + + return strings.TrimSuffix(string(data), "\n") + } + funcMap["chainRemoveField"] = func(u map[string]interface{}, fields string) map[string]interface{} { + unstructured.RemoveNestedField(u, strings.Split(fields, ".")...) + + return u + } + funcMap["chainSetField"] = func(u map[string]interface{}, fields string, value any) map[string]interface{} { + err := unstructured.SetNestedField(u, value, strings.Split(fields, ".")...) + if err != nil { + // Swallow errors inside of a template. + logger.V(logs.LogInfo).Info(fmt.Sprintf("failed with err %v", err)) + return nil + } + + return u + } templateName := getTemplateName(clusterNamespace, clusterName, requestorName) tmpl, err := template.New(templateName).Option("missingkey=error").Funcs(funcMap).Parse(values) @@ -217,3 +287,13 @@ func instantiateTemplateValues(ctx context.Context, config *rest.Config, c clien func getTemplateName(clusterNamespace, clusterName, requestorName string) string { return fmt.Sprintf("%s-%s-%s", clusterNamespace, clusterName, requestorName) } + +func resetFields(u map[string]interface{}) unstructured.Unstructured { + var uObject unstructured.Unstructured + uObject.SetUnstructuredContent(u) + uObject.SetManagedFields(nil) + uObject.SetResourceVersion("") + uObject.SetUID("") + + return uObject +} diff --git a/controllers/template_instantiation_test.go b/controllers/template_instantiation_test.go index 0844ec57..987e100d 100644 --- a/controllers/template_instantiation_test.go +++ b/controllers/template_instantiation_test.go @@ -19,9 +19,13 @@ package controllers_test import ( "context" "fmt" + "strconv" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -31,6 +35,7 @@ import ( "github.com/projectsveltos/addon-controller/controllers" libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1" + "github.com/projectsveltos/libsveltos/lib/utils" ) var _ = Describe("Template instantiation", func() { @@ -90,6 +95,286 @@ var _ = Describe("Template instantiation", func() { Expect(result).To(ContainSubstring(fmt.Sprintf("%s-test", cluster.Name))) }) + It("instantiateTemplateValues with getField", func() { + values := `{{ $replicasValue := getField "Deployment" "spec.replicas" }} +{{ toYaml $replicasValue }}` + + var replicas int32 = 3 + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: randomString(), + Namespace: randomString(), + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + }, + } + + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(depl) + Expect(err).To(BeNil()) + u := &unstructured.Unstructured{} + u.SetUnstructuredContent(content) + + mgmtResources := map[string]*unstructured.Unstructured{ + "Deployment": u, + } + + result, err := controllers.InstantiateTemplateValues(context.TODO(), testEnv.Config, testEnv.GetClient(), + libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name, randomString(), values, + mgmtResources, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + value, err := strconv.Atoi(strings.ReplaceAll(result, "\n", "")) + Expect(err).To(BeNil()) + Expect(value).To(Equal(3)) + }) + + It("instantiateTemplateValues with setField with int, string and bool", func() { + values := `{{ setField "Deployment" "spec.replicas" (int64 7) }}` + + var replicas int32 = 3 + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: randomString(), + Namespace: randomString(), + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + }, + } + Expect(addTypeInformationToObject(scheme, depl)).To(Succeed()) + + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(depl) + Expect(err).To(BeNil()) + u := &unstructured.Unstructured{} + u.SetUnstructuredContent(content) + + mgmtResources := map[string]*unstructured.Unstructured{ + "Deployment": u, + } + + result, err := controllers.InstantiateTemplateValues(context.TODO(), testEnv.Config, testEnv.GetClient(), + libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name, randomString(), values, + mgmtResources, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + Expect(result).To(ContainSubstring("replicas: 7")) + + modifiedDepl := appsv1.Deployment{} + Expect(yaml.Unmarshal([]byte(result), &modifiedDepl)).To(Succeed()) + Expect(modifiedDepl.Spec.Replicas).ToNot(BeNil()) + + values = `{{ setField "Deployment" "spec.paused" false }}` + result, err = controllers.InstantiateTemplateValues(context.TODO(), testEnv.Config, testEnv.GetClient(), + libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name, randomString(), values, + mgmtResources, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + Expect(result).To(ContainSubstring("paused: false")) + + Expect(yaml.Unmarshal([]byte(result), &modifiedDepl)).To(Succeed()) + Expect(modifiedDepl.Spec.Paused).To(BeFalse()) + + namespace := randomString() + values = fmt.Sprintf(`{{ setField "Deployment" "metadata.namespace" %q }}`, namespace) + + result, err = controllers.InstantiateTemplateValues(context.TODO(), testEnv.Config, testEnv.GetClient(), + libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name, randomString(), values, + mgmtResources, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + Expect(result).To(ContainSubstring(fmt.Sprintf("namespace: %s", namespace))) + + policy, err := utils.GetUnstructured([]byte(result)) + Expect(err).To(BeNil()) + Expect(policy.GetNamespace()).To(Equal(namespace)) + }) + + It("instantiateTemplateValues with setField with slice", func() { + // When dealing with slice, get slice (line 1) + // Create new slice modifying image value + // Use this modified slice, to set the field in the original struct + values := `{{ $currentContainers := (getField "Deployment" "spec.template.spec.containers") }} +{{ $modifiedContainers := list }} +{{- range $element := $currentContainers }} + {{ $modifiedContainers = append $modifiedContainers (chainSetField $element "image" "nginx:1.13" ) }} +{{- end }} +{{ setField "Deployment" "spec.template.spec.containers" $modifiedContainers }}` + + var replicas int32 = 3 + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: randomString(), + Namespace: randomString(), + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: randomString(), + Image: randomString(), + ImagePullPolicy: corev1.PullAlways, + }, + }, + ServiceAccountName: randomString(), + }, + }, + }, + } + Expect(addTypeInformationToObject(scheme, depl)).To(Succeed()) + + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(depl) + Expect(err).To(BeNil()) + u := &unstructured.Unstructured{} + u.SetUnstructuredContent(content) + + mgmtResources := map[string]*unstructured.Unstructured{ + "Deployment": u, + } + + result, err := controllers.InstantiateTemplateValues(context.TODO(), testEnv.Config, testEnv.GetClient(), + libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name, randomString(), values, + mgmtResources, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + + modifiedDepl := &appsv1.Deployment{} + Expect(yaml.Unmarshal([]byte(result), modifiedDepl)).To(Succeed()) + Expect(len(modifiedDepl.Spec.Template.Spec.Containers)).To(Equal(1)) + Expect(modifiedDepl.Spec.Template.Spec.Containers[0].Image).To(Equal("nginx:1.13")) + }) + + It("instantiateTemplateValues with setField with a map", func() { + values := `{{ $data := (getField "ConfigMap" "data")}} +{{ $data = (chainRemoveField $data "id") }} +{{ setField "ConfigMap" "data" $data }}` + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: randomString(), + Namespace: randomString(), + }, + Data: map[string]string{ + "user": randomString(), + "id": randomString(), + }, + } + Expect(addTypeInformationToObject(scheme, cm)).To(Succeed()) + + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cm) + Expect(err).To(BeNil()) + u := &unstructured.Unstructured{} + u.SetUnstructuredContent(content) + + mgmtResources := map[string]*unstructured.Unstructured{ + "ConfigMap": u, + } + + result, err := controllers.InstantiateTemplateValues(context.TODO(), testEnv.Config, testEnv.GetClient(), + libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name, randomString(), values, + mgmtResources, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + Expect(result).ToNot(ContainSubstring("replicas")) + + modifiedCm := &corev1.ConfigMap{} + Expect(yaml.Unmarshal([]byte(result), modifiedCm)).To(Succeed()) + Expect(modifiedCm.Data).ToNot(BeNil()) + Expect(modifiedCm.Data["user"]).ToNot(BeEmpty()) + Expect(modifiedCm.Data["id"]).To(BeEmpty()) + }) + + It("instantiateTemplateValues with removeField", func() { + values := `{{ removeField "Deployment" "spec.replicas" }}` + + var replicas int32 = 3 + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: randomString(), + Namespace: randomString(), + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + }, + } + Expect(addTypeInformationToObject(scheme, depl)).To(Succeed()) + + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(depl) + Expect(err).To(BeNil()) + u := &unstructured.Unstructured{} + u.SetUnstructuredContent(content) + + mgmtResources := map[string]*unstructured.Unstructured{ + "Deployment": u, + } + + result, err := controllers.InstantiateTemplateValues(context.TODO(), testEnv.Config, testEnv.GetClient(), + libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name, randomString(), values, + mgmtResources, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + Expect(result).ToNot(ContainSubstring("replicas")) + + modifiedDepl := &appsv1.Deployment{} + Expect(yaml.Unmarshal([]byte(result), modifiedDepl)).To(Succeed()) + Expect(modifiedDepl.Spec.Replicas).To(BeNil()) + }) + + It("instantiateTemplateValues with chainSetField", func() { + values := `{{ $depl := (getResource "Deployment") }} +{{ $depl := (chainSetField $depl "spec.replicas" (int64 5) ) }} +{{ $depl := (chainSetField $depl "metadata.namespace" .Cluster.metadata.namespace ) }} +{{ $depl := (chainSetField $depl "spec.template.spec.serviceAccountName" "default" ) }} +{{ $depl := (chainSetField $depl "spec.paused" true ) }} +{{ toYaml $depl }}` + + var replicas int32 = 3 + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: randomString(), + Namespace: randomString(), + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: randomString(), + Image: randomString(), + ImagePullPolicy: corev1.PullAlways, + }, + }, + ServiceAccountName: randomString(), + }, + }, + }, + } + Expect(addTypeInformationToObject(scheme, depl)).To(Succeed()) + + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(depl) + Expect(err).To(BeNil()) + u := &unstructured.Unstructured{} + u.SetUnstructuredContent(content) + + mgmtResources := map[string]*unstructured.Unstructured{ + "Deployment": u, + } + + result, err := controllers.InstantiateTemplateValues(context.TODO(), testEnv.Config, testEnv.GetClient(), + libsveltosv1beta1.ClusterTypeCapi, cluster.Namespace, cluster.Name, randomString(), values, + mgmtResources, textlogger.NewLogger(textlogger.NewConfig())) + Expect(err).To(BeNil()) + Expect(result).To(ContainSubstring("replicas: 5")) + Expect(result).To(ContainSubstring(fmt.Sprintf("namespace: %s", cluster.Namespace))) + Expect(result).To(ContainSubstring("paused: true")) + + modifiedDepl := appsv1.Deployment{} + Expect(yaml.Unmarshal([]byte(result), &modifiedDepl)).To(Succeed()) + Expect(modifiedDepl.Spec.Replicas).ToNot(BeNil()) + Expect(*modifiedDepl.Spec.Replicas).To(Equal(int32(5))) + Expect(modifiedDepl.Spec.Paused).To(BeTrue()) + + policy, err := utils.GetUnstructured([]byte(result)) + Expect(err).To(BeNil()) + Expect(policy.GetNamespace()).To(Equal(namespace)) + }) + It("instantiateTemplateValues returns correct values (spec section)", func() { values := `valuesTemplate: | controller: diff --git a/go.sum b/go.sum index 16502f37..10a941a5 100644 --- a/go.sum +++ b/go.sum @@ -326,8 +326,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/projectsveltos/libsveltos v0.42.0 h1:U+mFYi2K4IPY1rvpg4jAPBqG4PaznUK4huo/xnFk7jc= -github.com/projectsveltos/libsveltos v0.42.0/go.mod h1:DvifWRZuPtGKi4oGzQzcGe8XBKlzIlx2DcaoukAio+M= github.com/projectsveltos/libsveltos v0.42.1 h1:B7c8cF+vhR5AZJT8D4h19PSJfZqWUCR3CKEDUf032JI= github.com/projectsveltos/libsveltos v0.42.1/go.mod h1:XPev2TKsMxVG5LwhbbMkcCs/U0730ZMmOXIvu2HEtWo= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -563,8 +561,6 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 h1:MErs8YA0abvOqJ8gIupA1Tz6PKXYUw34XsGlA7uSL1k= k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094/go.mod h1:7ioBJr1A6igWjsR2fxq2EZ0mlMwYLejazSIc2bzMp2U= -k8s.io/kubectl v0.31.2 h1:gTxbvRkMBwvTSAlobiTVqsH6S8Aa1aGyBcu5xYLsn8M= -k8s.io/kubectl v0.31.2/go.mod h1:EyASYVU6PY+032RrTh5ahtSOMgoDRIux9V1JLKtG5xM= k8s.io/kubectl v0.31.3 h1:3r111pCjPsvnR98oLLxDMwAeM6OPGmPty6gSKaLTQes= k8s.io/kubectl v0.31.3/go.mod h1:lhMECDCbJN8He12qcKqs2QfmVo9Pue30geovBVpH5fs= k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno= @@ -575,8 +571,6 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsA sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/cluster-api v1.8.5 h1:lNA2fPN4fkXEs+oOQlnwxT/4VwRFBpv5kkSoJG8nqBA= sigs.k8s.io/cluster-api v1.8.5/go.mod h1:pXv5LqLxuIbhGIXykyNKiJh+KrLweSBajVHHitPLyoY= -sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= -sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/controller-runtime v0.19.2 h1:3sPrF58XQEPzbE8T81TN6selQIMGbtYwuaJ6eDssDF8= sigs.k8s.io/controller-runtime v0.19.2/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=