From da9f902d269123803c72e3347027dcfefc07d647 Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Fri, 19 May 2023 10:22:18 +0200 Subject: [PATCH 1/9] Separate resource state from template state --- controllers/statuscheck.go | 10 ++--- controllers/templating.go | 78 ++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/controllers/statuscheck.go b/controllers/statuscheck.go index 8e8f530f..8d1f021b 100644 --- a/controllers/statuscheck.go +++ b/controllers/statuscheck.go @@ -38,12 +38,12 @@ import ( // Given a slice of template states, compute the overall // health/readiness status. The general approach is to test for a // `Ready` status condition, which is implemented through kstatus. -func statusIsReady(templates []*TemplateResource) (bool, error) { +func statusIsReady(templates []*ResourceTemplateState) (bool, error) { for _, tmplRes := range templates { - if tmplRes.Current == nil { + if tmplRes.Resource.Current == nil { return false, nil } - res, err := status.Compute(tmplRes.Current) + res, err := status.Compute(tmplRes.Resource.Current) if err != nil { return false, err } @@ -55,10 +55,10 @@ func statusIsReady(templates []*TemplateResource) (bool, error) { } // Build a list of template names which are not yet reconciled. Useful for status reporting -func statusExistingTemplates(templates []*TemplateResource) []string { +func statusExistingTemplates(templates []*ResourceTemplateState) []string { var missing []string for _, tmplRes := range templates { - if tmplRes.Current == nil { + if tmplRes.Resource.Current == nil { missing = append(missing, tmplRes.TemplateName) } } diff --git a/controllers/templating.go b/controllers/templating.go index c09ed496..02bd8448 100644 --- a/controllers/templating.go +++ b/controllers/templating.go @@ -51,15 +51,10 @@ import ( sigsyaml "sigs.k8s.io/yaml" ) -// Rendering and applying templates is a multi-stage process. This -// structure holds information about a rendered template between these -// two stages -type TemplateResource struct { - // Compiled template - Template *template.Template - +// Information about a resource, rendered format as well as actual in API server +type Composite struct { // The rendered resource - Resource *unstructured.Unstructured + Rendered *unstructured.Unstructured // GVR for resource GVR *schema.GroupVersionResource @@ -67,14 +62,25 @@ type TemplateResource struct { // Current resource fetch from API-server (or as close as our local caching allows) Current *unstructured.Unstructured + // Whether resource is namespaced or not + IsNamespaced bool +} + +// Rendering and applying templates is a multi-stage process. This +// structure holds information about a rendered template between +// stages +type ResourceTemplateState struct { + // Compiled template + Template *template.Template + + // Resource information, rendered and current + Resource Composite + // Name of rendered resource (from template key in GatewayClassBlueprint, not Kubernetes resource name) TemplateName string // Raw template for resource StringTemplate string - - // Whether resource is namespaced or not - IsNamespaced bool } // Parameters used when rendering templates @@ -113,14 +119,14 @@ func parseSingleTemplate(tmplKey, tmpl string) (*template.Template, error) { return template.New(tmplKey).Option("missingkey=error").Funcs(sprig.TxtFuncMap()).Funcs(funcs).Parse(tmpl) } -// Initialize TemplateResource slice by parsing templates -func parseTemplates(resourceTemplates map[string]string) ([]*TemplateResource, error) { +// Initialize ResourceTemplateState slice by parsing templates +func parseTemplates(resourceTemplates map[string]string) ([]*ResourceTemplateState, error) { var err error - templates := make([]*TemplateResource, 0, len(resourceTemplates)) + templates := make([]*ResourceTemplateState, 0, len(resourceTemplates)) for tmplKey, tmpl := range resourceTemplates { - r := TemplateResource{} + r := ResourceTemplateState{} r.TemplateName = tmplKey r.StringTemplate = tmpl r.Template, err = parseSingleTemplate(tmplKey, tmpl) @@ -139,15 +145,15 @@ func parseTemplates(resourceTemplates map[string]string) ([]*TemplateResource, e // render the template first. Rendering errors on final attempt are // logged as errors. func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.Object, - templates []*TemplateResource, values *TemplateValues, isFinalAttempt bool) (rendered, exists int) { + templates []*ResourceTemplateState, values *TemplateValues, isFinalAttempt bool) (rendered, exists int) { var err error logger := log.FromContext(ctx) ns := parent.GetNamespace() for _, tmplRes := range templates { - if tmplRes.Resource == nil { - tmplRes.Resource, err = template2Unstructured(tmplRes.Template, values) + if tmplRes.Resource.Rendered == nil { + tmplRes.Resource.Rendered, err = template2Unstructured(tmplRes.Template, values) if err != nil { if isFinalAttempt { logger.Error(err, "cannot render template", "templateName", tmplRes.TemplateName) @@ -158,29 +164,29 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O continue } } - if tmplRes.GVR == nil { - tmplRes.GVR, tmplRes.IsNamespaced, err = unstructuredToGVR(r, tmplRes.Resource) + if tmplRes.Resource.GVR == nil { + tmplRes.Resource.GVR, tmplRes.Resource.IsNamespaced, err = unstructuredToGVR(r, tmplRes.Resource.Rendered) if err != nil { logger.Error(err, "cannot detect GVR for resource", "templateName", tmplRes.TemplateName) continue } } rendered++ - if tmplRes.Current == nil { + if tmplRes.Resource.Current == nil { var dynamicClient dynamic.ResourceInterface - if tmplRes.IsNamespaced { - dynamicClient = r.DynamicClient().Resource(*tmplRes.GVR).Namespace(ns) + if tmplRes.Resource.IsNamespaced { + dynamicClient = r.DynamicClient().Resource(*tmplRes.Resource.GVR).Namespace(ns) } else { - dynamicClient = r.DynamicClient().Resource(*tmplRes.GVR) + dynamicClient = r.DynamicClient().Resource(*tmplRes.Resource.GVR) } - tmplRes.Current, err = dynamicClient.Get(ctx, tmplRes.Resource.GetName(), metav1.GetOptions{}) + tmplRes.Resource.Current, err = dynamicClient.Get(ctx, tmplRes.Resource.Rendered.GetName(), metav1.GetOptions{}) if err != nil { logger.Error(err, "cannot get current resource", "templateName", tmplRes.TemplateName) continue } - logger.Info("update current", "templatename", tmplRes.TemplateName, "current", tmplRes.Current) + logger.Info("update current", "templatename", tmplRes.TemplateName, "current", tmplRes.Resource.Current) } else { - logger.Info("already have update current", "templatename", tmplRes.TemplateName, "current", tmplRes.Current) + logger.Info("already have update current", "templatename", tmplRes.TemplateName, "current", tmplRes.Resource.Current) } exists++ } @@ -190,12 +196,12 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O // Build a map of values from current resources. Useful for // referencing values between resources, e.g. a status field from one // resource may be used to template another resource -func buildResourceValues(templates []*TemplateResource) map[string]any { +func buildResourceValues(templates []*ResourceTemplateState) map[string]any { resources := map[string]any{} for _, tmplRes := range templates { - if tmplRes.Current != nil { - resources[tmplRes.TemplateName] = tmplRes.Current.UnstructuredContent() + if tmplRes.Resource.Current != nil { + resources[tmplRes.TemplateName] = tmplRes.Resource.Current.UnstructuredContent() } } return resources @@ -203,33 +209,33 @@ func buildResourceValues(templates []*TemplateResource) map[string]any { // Apply a list of pre-rendered templates and set owner reference for // namespaced resources -func applyTemplates(ctx context.Context, r ControllerDynClient, parent metav1.Object, templates []*TemplateResource) error { +func applyTemplates(ctx context.Context, r ControllerDynClient, parent metav1.Object, templates []*ResourceTemplateState) error { var err error var errorCnt = 0 logger := log.FromContext(ctx) for _, tmplRes := range templates { - if tmplRes.Resource == nil || tmplRes.GVR == nil { + if tmplRes.Resource.Rendered == nil || tmplRes.Resource.GVR == nil { // We do not yet have enough information to render/apply this resource continue } - if tmplRes.IsNamespaced { + if tmplRes.Resource.IsNamespaced { // Only namespaced objects can have namespaced object as owner - err = ctrl.SetControllerReference(parent, tmplRes.Resource, r.Scheme()) + err = ctrl.SetControllerReference(parent, tmplRes.Resource.Rendered, r.Scheme()) if err != nil { logger.Error(err, "cannot set owner for namespaced template", "templateName", tmplRes.TemplateName) errorCnt++ } else { ns := parent.GetNamespace() - err = patchUnstructured(ctx, r, tmplRes.Resource, tmplRes.GVR, &ns) + err = patchUnstructured(ctx, r, tmplRes.Resource.Rendered, tmplRes.Resource.GVR, &ns) if err != nil { logger.Error(err, "cannot apply namespaced template", "templateName", tmplRes.TemplateName) errorCnt++ } } } else { - err = patchUnstructured(ctx, r, tmplRes.Resource, tmplRes.GVR, nil) + err = patchUnstructured(ctx, r, tmplRes.Resource.Rendered, tmplRes.Resource.GVR, nil) if err != nil { logger.Error(err, "cannot apply cluster-scoped template", "templateName", tmplRes.TemplateName) errorCnt++ From 17bdfc51e3bbe831c9ea63d844681a43983842e5 Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Fri, 19 May 2023 13:26:02 +0200 Subject: [PATCH 2/9] Partial multi-resource template refactoring and tests --- controllers/gateway_controller.go | 6 +- controllers/statuscheck.go | 32 ++++---- controllers/templating.go | 55 +++++++++----- controllers/templating_test.go | 119 ++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 controllers/templating_test.go diff --git a/controllers/gateway_controller.go b/controllers/gateway_controller.go index 6822da66..5d39bbc1 100644 --- a/controllers/gateway_controller.go +++ b/controllers/gateway_controller.go @@ -186,13 +186,13 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if tmpl, errs := parseSingleTemplate("status", tmplStr); errs != nil { logger.Info("unable to parse status template", "temporary error", errs) } else { - if statusMap, errs := template2map(tmpl, &templateValues); errs != nil { + if statusMap, errs := template2maps(tmpl, &templateValues); errs != nil { logger.Info("unable to render status template", "temporary error", errs, "template", tmplStr, "values", templateValues) } else { gw.Status.Addresses = []gatewayapi.GatewayAddress{} - _, found := statusMap["addresses"] + _, found := statusMap[0]["addresses"] // FIXME, more addresses? if found { - addresses := statusMap["addresses"] + addresses := statusMap[0]["addresses"] if errs := mapstructure.Decode(addresses, &gw.Status.Addresses); errs != nil { // This is probably not a temporary error logger.Error(errs, "unable to decode status data") diff --git a/controllers/statuscheck.go b/controllers/statuscheck.go index 8d1f021b..774e2be7 100644 --- a/controllers/statuscheck.go +++ b/controllers/statuscheck.go @@ -32,6 +32,8 @@ limitations under the License. package controllers import ( + "fmt" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" ) @@ -39,16 +41,18 @@ import ( // health/readiness status. The general approach is to test for a // `Ready` status condition, which is implemented through kstatus. func statusIsReady(templates []*ResourceTemplateState) (bool, error) { - for _, tmplRes := range templates { - if tmplRes.Resource.Current == nil { - return false, nil - } - res, err := status.Compute(tmplRes.Resource.Current) - if err != nil { - return false, err - } - if res.Status != status.CurrentStatus { - return false, nil + for _, tmpl := range templates { + for _, res := range tmpl.NewResource { + if res.Current == nil { + return false, nil + } + res, err := status.Compute(res.Current) + if err != nil { + return false, err + } + if res.Status != status.CurrentStatus { + return false, nil + } } } return true, nil @@ -57,9 +61,11 @@ func statusIsReady(templates []*ResourceTemplateState) (bool, error) { // Build a list of template names which are not yet reconciled. Useful for status reporting func statusExistingTemplates(templates []*ResourceTemplateState) []string { var missing []string - for _, tmplRes := range templates { - if tmplRes.Resource.Current == nil { - missing = append(missing, tmplRes.TemplateName) + for _, tmpl := range templates { + for resIdx, res := range tmpl.NewResource { + if res.Current == nil { + missing = append(missing, fmt.Sprintf("%s[%d]", tmpl.TemplateName, resIdx)) + } } } return missing diff --git a/controllers/templating.go b/controllers/templating.go index 02bd8448..ee384195 100644 --- a/controllers/templating.go +++ b/controllers/templating.go @@ -36,6 +36,7 @@ import ( "context" "fmt" "io" + "sort" "strings" "text/template" @@ -52,7 +53,7 @@ import ( ) // Information about a resource, rendered format as well as actual in API server -type Composite struct { +type ResourceComposite struct { // The rendered resource Rendered *unstructured.Unstructured @@ -67,19 +68,19 @@ type Composite struct { } // Rendering and applying templates is a multi-stage process. This -// structure holds information about a rendered template between -// stages +// structure holds information about a template between stages type ResourceTemplateState struct { // Compiled template Template *template.Template // Resource information, rendered and current - Resource Composite + Resource ResourceComposite // FIXME, refactoring - delete and replace with below + NewResource []ResourceComposite // FIXME, refactoring temp name - // Name of rendered resource (from template key in GatewayClassBlueprint, not Kubernetes resource name) + // Name of template (from template key in GatewayClassBlueprint, not Kubernetes resource name) TemplateName string - // Raw template for resource + // Raw template StringTemplate string } @@ -133,9 +134,13 @@ func parseTemplates(resourceTemplates map[string]string) ([]*ResourceTemplateSta if err != nil { return nil, fmt.Errorf("cannot parse template %q: %w", tmplKey, err) } + r.NewResource = make([]ResourceComposite, 1) templates = append(templates, &r) } + // Sort to increase predictability + sort.SliceStable(templates, func(i, j int) bool { return templates[i].TemplateName < templates[j].TemplateName }) + return templates, nil } @@ -153,7 +158,7 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O for _, tmplRes := range templates { if tmplRes.Resource.Rendered == nil { - tmplRes.Resource.Rendered, err = template2Unstructured(tmplRes.Template, values) + //tmplRes.Resource.Rendered, err = template2Unstructured(tmplRes.Template, values) // FIXME if err != nil { if isFinalAttempt { logger.Error(err, "cannot render template", "templateName", tmplRes.TemplateName) @@ -199,9 +204,9 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O func buildResourceValues(templates []*ResourceTemplateState) map[string]any { resources := map[string]any{} - for _, tmplRes := range templates { - if tmplRes.Resource.Current != nil { - resources[tmplRes.TemplateName] = tmplRes.Resource.Current.UnstructuredContent() + for _, tmpl := range templates { + if tmpl.Resource.Current != nil { + resources[tmpl.TemplateName] = tmpl.Resource.Current.UnstructuredContent() } } return resources @@ -272,26 +277,38 @@ func templateRender(tmpl *template.Template, templateValues *TemplateValues) (*b return &buffer, nil } -func template2map(tmpl *template.Template, tmplValues *TemplateValues) (map[string]any, error) { +func template2maps(tmpl *template.Template, tmplValues *TemplateValues) ([]map[string]any, error) { renderBuffer, err := templateRender(tmpl, tmplValues) if err != nil { return nil, err } - rawResource := map[string]any{} - err = yaml.Unmarshal(renderBuffer.Bytes(), &rawResource) - if err != nil { - return nil, err + rawSlice := bytes.SplitN(renderBuffer.Bytes(), []byte("---"), -1) + resources := make([]map[string]any, 0, len(rawSlice)) + for _, raw := range rawSlice { + r := map[string]any{} + err = yaml.Unmarshal(raw, &r) + if err != nil { + return nil, err + } + if len(r) == 0 { + continue // Empty resource + } + resources = append(resources, r) } - return rawResource, nil + return resources, nil } -func template2Unstructured(tmpl *template.Template, tmplValues *TemplateValues) (*unstructured.Unstructured, error) { - rawResource, err := template2map(tmpl, tmplValues) +func template2Unstructured(tmpl *template.Template, tmplValues *TemplateValues) ([]unstructured.Unstructured, error) { + rawResources, err := template2maps(tmpl, tmplValues) if err != nil { return nil, err } - return &unstructured.Unstructured{Object: rawResource}, nil + uu := make([]unstructured.Unstructured, 0, len(rawResources)) + for _, r := range rawResources { + uu = append(uu, unstructured.Unstructured{Object: r}) + } + return uu, nil } // Prepare a resource like Gateway or HTTPRoute for use in templates diff --git a/controllers/templating_test.go b/controllers/templating_test.go new file mode 100644 index 00000000..914d7238 --- /dev/null +++ b/controllers/templating_test.go @@ -0,0 +1,119 @@ +package controllers + +import ( + "k8s.io/apimachinery/pkg/util/yaml" + "testing" +) + +func TestParseSingleTemplate(t *testing.T) { + template := "foo" + tmpl, err := parseSingleTemplate("foo", template) + if tmpl == nil || err != nil { + t.Fatalf("Error parsing template %v", err) + } +} + +var textTemplate = ` +t1: | + name: {{ .Values.name1 }} +t2: | + {{ if .Values.t2enable }} + name: {{ .Values.name2 }} + {{ end }} +t3: | + {{ range .Values.t3data }} + name: {{ $.Values.name3 }}-{{ . }} + --- + {{ end }} +` + +var textValues = ` +name1: t1name +name2: t2name +name3: t3name +t2enable: false +t3data: +- foo1 +- foo2 +- foo3 +` + +func helperGetResourceState() ([]*ResourceTemplateState, error) { + templates := map[string]string{} + _ = yaml.Unmarshal([]byte(textTemplate), &templates) + return parseTemplates(templates) +} + +func helperGetValues() *TemplateValues { + values := map[string]any{} + _ = yaml.Unmarshal([]byte(textValues), &values) + templateValues := TemplateValues{ + Values: values, + } + return &templateValues +} + +func TestParseTemplate(t *testing.T) { + tmpl, err := helperGetResourceState() + if tmpl == nil || err != nil { + t.Fatalf("Error parsing templates %v", err) + } + if len(tmpl) != 2 { + t.Fatalf("Template slice lenght mismatch, got %v, expected 2", len(tmpl)) + } + if tmpl[0].TemplateName != "t1" { + t.Fatalf("Template[0] name, got %v, expected t1", tmpl[0].TemplateName) + } +} + +func TestTemplate2map(t *testing.T) { + tmpl, err := helperGetResourceState() + tmplValues := helperGetValues() + rawResources, err := template2maps(tmpl[0].Template, tmplValues) + if rawResources == nil { + t.Fatalf("Cannot render template to map: %v", err) + } + if len(rawResources) != 1 { + t.Fatalf("Error rendering resource, got len %v, expected 1", len(rawResources)) + } + if rawResources[0]["name"] != "t1name" { + t.Fatalf("Rendered template error, got %v, expected 't1name'", rawResources[0]["name"]) + } + rawResources, err = template2maps(tmpl[1].Template, tmplValues) + if err != nil { + t.Fatalf("Error rendering empty resource, got err %v", err) + } + if len(rawResources) != 0 { + t.Fatalf("Error rendering empty resource, got len %v, expected 0", len(rawResources)) + } + rawResources, err = template2maps(tmpl[2].Template, tmplValues) + if err != nil { + t.Fatalf("Error rendering multi-resource, got err %v", err) + } + if len(rawResources) != 3 { + t.Fatalf("Error rendering multi-resource, got len %v, expected 3", len(rawResources)) + } +} + +func TestTemplate2Unstructured(t *testing.T) { + tmpl, err := helperGetResourceState() + tmplValues := helperGetValues() + u, err := template2Unstructured(tmpl[0].Template, tmplValues) + if u == nil { + t.Fatalf("Cannot render template to map: %v", err) + } + u, err = template2Unstructured(tmpl[1].Template, tmplValues) + if err != nil { + t.Fatalf("Error rendering empty resource, got err %v", err) + } + if len(u) != 0 { + t.Fatalf("Error rendering empty resource, got %v, expected 0", len(u)) + } + u, err = template2Unstructured(tmpl[2].Template, tmplValues) + if err != nil { + t.Fatalf("Error rendering multi-resource, got err %v", err) + } + if len(u) != 3 { + t.Fatalf("Error rendering multi-resource, got len %v, expected 3", len(u)) + } +} From 02f714da5e3a4acbaeff35956bd3fadc3028b2bd Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Fri, 19 May 2023 14:21:38 +0200 Subject: [PATCH 3/9] Checkpoint, not building --- controllers/templating.go | 88 +++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/controllers/templating.go b/controllers/templating.go index ee384195..3dcfa63b 100644 --- a/controllers/templating.go +++ b/controllers/templating.go @@ -74,7 +74,7 @@ type ResourceTemplateState struct { Template *template.Template // Resource information, rendered and current - Resource ResourceComposite // FIXME, refactoring - delete and replace with below + OldResource ResourceComposite // FIXME, refactoring - delete and replace with below NewResource []ResourceComposite // FIXME, refactoring temp name // Name of template (from template key in GatewayClassBlueprint, not Kubernetes resource name) @@ -156,42 +156,45 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O logger := log.FromContext(ctx) ns := parent.GetNamespace() - for _, tmplRes := range templates { - if tmplRes.Resource.Rendered == nil { - //tmplRes.Resource.Rendered, err = template2Unstructured(tmplRes.Template, values) // FIXME + for _, tmpl := range templates { + if len(tmpl.NewResource) == 0 { + tmpl.NewResource, err = template2Composite(r, tmpl.Template, values) if err != nil { if isFinalAttempt { - logger.Error(err, "cannot render template", "templateName", tmplRes.TemplateName) + logger.Error(err, "cannot render template", "templateName", tmpl.TemplateName) // FIXME: These are convenient, but we should have a better logging design, i.e. it should be possible to enable rendering errors only - fmt.Printf("Template:\n%s\n", tmplRes.StringTemplate) + fmt.Printf("Template:\n%s\n", tmpl.StringTemplate) fmt.Printf("Template values:\n%+v\n", values) } continue } } - if tmplRes.Resource.GVR == nil { - tmplRes.Resource.GVR, tmplRes.Resource.IsNamespaced, err = unstructuredToGVR(r, tmplRes.Resource.Rendered) - if err != nil { - logger.Error(err, "cannot detect GVR for resource", "templateName", tmplRes.TemplateName) - continue - } - } + // FIXME, remove + // if tmplRes.Resource.GVR == nil { + // tmplRes.Resource.GVR, tmplRes.Resource.IsNamespaced, err = unstructuredToGVR(r, tmplRes.Resource.Rendered) + // if err != nil { + // logger.Error(err, "cannot detect GVR for resource", "templateName", tmplRes.TemplateName) + // continue + // } + // } rendered++ - if tmplRes.Resource.Current == nil { - var dynamicClient dynamic.ResourceInterface - if tmplRes.Resource.IsNamespaced { - dynamicClient = r.DynamicClient().Resource(*tmplRes.Resource.GVR).Namespace(ns) + for resIdx, res := range tmpl.NewResource { + if res.Current == nil { + var dynamicClient dynamic.ResourceInterface + if res.IsNamespaced { + dynamicClient = r.DynamicClient().Resource(*res.GVR).Namespace(ns) + } else { + dynamicClient = r.DynamicClient().Resource(*res.GVR) + } + res.Current, err = dynamicClient.Get(ctx, res.Rendered.GetName(), metav1.GetOptions{}) + if err != nil { + logger.Error(err, "cannot get current resource", "templateName", tmpl.TemplateName, "resIdx", resIdx) + continue + } + logger.Info("update current", "templatename", tmpl.TemplateName, "current", res.Current) } else { - dynamicClient = r.DynamicClient().Resource(*tmplRes.Resource.GVR) + logger.Info("already have update current", "templatename", tmpl.TemplateName, "current", res.Current) } - tmplRes.Resource.Current, err = dynamicClient.Get(ctx, tmplRes.Resource.Rendered.GetName(), metav1.GetOptions{}) - if err != nil { - logger.Error(err, "cannot get current resource", "templateName", tmplRes.TemplateName) - continue - } - logger.Info("update current", "templatename", tmplRes.TemplateName, "current", tmplRes.Resource.Current) - } else { - logger.Info("already have update current", "templatename", tmplRes.TemplateName, "current", tmplRes.Resource.Current) } exists++ } @@ -205,8 +208,11 @@ func buildResourceValues(templates []*ResourceTemplateState) map[string]any { resources := map[string]any{} for _, tmpl := range templates { - if tmpl.Resource.Current != nil { - resources[tmpl.TemplateName] = tmpl.Resource.Current.UnstructuredContent() + resources[tmpl.TemplateName] = make([]map[string]any, 0) + for _, res := range tmpl.NewResource { + if res.Current != nil { + resources[tmpl.TemplateName] = append(resources[tmpl.TemplateName], res) + } } } return resources @@ -299,16 +305,34 @@ func template2maps(tmpl *template.Template, tmplValues *TemplateValues) ([]map[s return resources, nil } -func template2Unstructured(tmpl *template.Template, tmplValues *TemplateValues) ([]unstructured.Unstructured, error) { +// FIXME delete replaced by template2Composite +// func template2Unstructured(tmpl *template.Template, tmplValues *TemplateValues) ([]unstructured.Unstructured, error) { +// rawResources, err := template2maps(tmpl, tmplValues) +// if err != nil { +// return nil, err +// } +// uu := make([]unstructured.Unstructured, 0, len(rawResources)) +// for _, r := range rawResources { +// uu = append(uu, unstructured.Unstructured{Object: r}) +// } +// return uu, nil +// } + +func template2Composite(r ControllerClient, tmpl *template.Template, tmplValues *TemplateValues) ([]ResourceComposite, error) { rawResources, err := template2maps(tmpl, tmplValues) if err != nil { return nil, err } - uu := make([]unstructured.Unstructured, 0, len(rawResources)) + composites := make([]ResourceComposite, 0, len(rawResources)) for _, r := range rawResources { - uu = append(uu, unstructured.Unstructured{Object: r}) + c := ResourceComposite{} + c.Rendered = &unstructured.Unstructured{Object: r} + c.GVR, c.IsNamespaced, err = unstructuredToGVR(r, c.Rendered) + if err != nil { + return nil, err + } } - return uu, nil + return composites, nil } // Prepare a resource like Gateway or HTTPRoute for use in templates From ad995e8d539877eb538551c1f80e2267ae15311b Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Mon, 22 May 2023 14:03:45 +0200 Subject: [PATCH 4/9] Tests passing --- controllers/gateway_controller_test.go | 6 +- controllers/statuscheck.go | 4 +- controllers/templating.go | 99 +++++++++++--------------- controllers/templating_test.go | 27 +------ 4 files changed, 49 insertions(+), 87 deletions(-) diff --git a/controllers/gateway_controller_test.go b/controllers/gateway_controller_test.go index 5d6e26c6..3b729d0c 100644 --- a/controllers/gateway_controller_test.go +++ b/controllers/gateway_controller_test.go @@ -85,7 +85,7 @@ spec: status: template: | addresses: - {{ toYaml .Resources.childGateway.status.addresses | nindent 2}} + {{ toYaml (index .Resources.childGateway 0).status.addresses | nindent 2}} resourceTemplates: childGateway: | apiVersion: gateway.networking.k8s.io/v1beta1 @@ -116,7 +116,7 @@ spec: name: intermediate-configmap namespace: {{ .Gateway.metadata.namespace }} data: - valueIntermediate: {{ .Resources.configMapTestSource.data.valueToRead1 }} + valueIntermediate: {{ (index .Resources.configMapTestSource 0).data.valueToRead1 }} # Use references to multiple resources coupled with template pipeline and functions configMapTestDestination: | apiVersion: v1 @@ -125,7 +125,7 @@ spec: name: dst-configmap namespace: {{ .Gateway.metadata.namespace }} data: - valueRead: {{ printf "%s, %s" .Resources.configMapTestIntermediate.data.valueIntermediate .Resources.configMapTestSource.data.valueToRead2 | upper }} + valueRead: {{ printf "%s, %s" (index .Resources.configMapTestIntermediate 0).data.valueIntermediate (index .Resources.configMapTestSource 0).data.valueToRead2 | upper }} httpRouteTemplate: resourceTemplates: shadowHttproute: | diff --git a/controllers/statuscheck.go b/controllers/statuscheck.go index 774e2be7..d29dd239 100644 --- a/controllers/statuscheck.go +++ b/controllers/statuscheck.go @@ -42,7 +42,7 @@ import ( // `Ready` status condition, which is implemented through kstatus. func statusIsReady(templates []*ResourceTemplateState) (bool, error) { for _, tmpl := range templates { - for _, res := range tmpl.NewResource { + for _, res := range tmpl.NewResources { if res.Current == nil { return false, nil } @@ -62,7 +62,7 @@ func statusIsReady(templates []*ResourceTemplateState) (bool, error) { func statusExistingTemplates(templates []*ResourceTemplateState) []string { var missing []string for _, tmpl := range templates { - for resIdx, res := range tmpl.NewResource { + for resIdx, res := range tmpl.NewResources { if res.Current == nil { missing = append(missing, fmt.Sprintf("%s[%d]", tmpl.TemplateName, resIdx)) } diff --git a/controllers/templating.go b/controllers/templating.go index 3dcfa63b..89b9ca21 100644 --- a/controllers/templating.go +++ b/controllers/templating.go @@ -74,8 +74,8 @@ type ResourceTemplateState struct { Template *template.Template // Resource information, rendered and current - OldResource ResourceComposite // FIXME, refactoring - delete and replace with below - NewResource []ResourceComposite // FIXME, refactoring temp name + OldResource ResourceComposite // FIXME, refactoring - delete and replace with below + NewResources []ResourceComposite // FIXME, refactoring temp name // Name of template (from template key in GatewayClassBlueprint, not Kubernetes resource name) TemplateName string @@ -134,7 +134,7 @@ func parseTemplates(resourceTemplates map[string]string) ([]*ResourceTemplateSta if err != nil { return nil, fmt.Errorf("cannot parse template %q: %w", tmplKey, err) } - r.NewResource = make([]ResourceComposite, 1) + r.NewResources = make([]ResourceComposite, 0) templates = append(templates, &r) } @@ -156,9 +156,10 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O logger := log.FromContext(ctx) ns := parent.GetNamespace() - for _, tmpl := range templates { - if len(tmpl.NewResource) == 0 { - tmpl.NewResource, err = template2Composite(r, tmpl.Template, values) + for tIdx := range templates { + tmpl := templates[tIdx] + if len(tmpl.NewResources) == 0 { + tmpl.NewResources, err = template2Composite(r, tmpl.Template, values) if err != nil { if isFinalAttempt { logger.Error(err, "cannot render template", "templateName", tmpl.TemplateName) @@ -169,16 +170,9 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O continue } } - // FIXME, remove - // if tmplRes.Resource.GVR == nil { - // tmplRes.Resource.GVR, tmplRes.Resource.IsNamespaced, err = unstructuredToGVR(r, tmplRes.Resource.Rendered) - // if err != nil { - // logger.Error(err, "cannot detect GVR for resource", "templateName", tmplRes.TemplateName) - // continue - // } - // } rendered++ - for resIdx, res := range tmpl.NewResource { + for resIdx := range tmpl.NewResources { + res := &tmpl.NewResources[resIdx] if res.Current == nil { var dynamicClient dynamic.ResourceInterface if res.IsNamespaced { @@ -191,9 +185,9 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O logger.Error(err, "cannot get current resource", "templateName", tmpl.TemplateName, "resIdx", resIdx) continue } - logger.Info("update current", "templatename", tmpl.TemplateName, "current", res.Current) + logger.Info("update current", "templatename", tmpl.TemplateName, "idx", resIdx, "current", res.Current) } else { - logger.Info("already have update current", "templatename", tmpl.TemplateName, "current", res.Current) + logger.Info("already have update current", "templatename", tmpl.TemplateName, "idx", resIdx, "current", res.Current) } } exists++ @@ -205,17 +199,18 @@ func renderTemplates(ctx context.Context, r ControllerDynClient, parent metav1.O // referencing values between resources, e.g. a status field from one // resource may be used to template another resource func buildResourceValues(templates []*ResourceTemplateState) map[string]any { - resources := map[string]any{} + resourceValues := map[string]any{} for _, tmpl := range templates { - resources[tmpl.TemplateName] = make([]map[string]any, 0) - for _, res := range tmpl.NewResource { + resSlice := make([]map[string]any, 0) + for _, res := range tmpl.NewResources { if res.Current != nil { - resources[tmpl.TemplateName] = append(resources[tmpl.TemplateName], res) + resSlice = append(resSlice, res.Current.UnstructuredContent()) } } + resourceValues[tmpl.TemplateName] = resSlice } - return resources + return resourceValues } // Apply a list of pre-rendered templates and set owner reference for @@ -226,31 +221,33 @@ func applyTemplates(ctx context.Context, r ControllerDynClient, parent metav1.Ob logger := log.FromContext(ctx) - for _, tmplRes := range templates { - if tmplRes.Resource.Rendered == nil || tmplRes.Resource.GVR == nil { - // We do not yet have enough information to render/apply this resource - continue - } - if tmplRes.Resource.IsNamespaced { - // Only namespaced objects can have namespaced object as owner - err = ctrl.SetControllerReference(parent, tmplRes.Resource.Rendered, r.Scheme()) - if err != nil { - logger.Error(err, "cannot set owner for namespaced template", "templateName", tmplRes.TemplateName) - errorCnt++ + for _, tmpl := range templates { + for _, res := range tmpl.NewResources { + if res.Rendered == nil || res.GVR == nil { + // We do not yet have enough information to render/apply this resource + continue + } + if res.IsNamespaced { + // Only namespaced objects can have namespaced object as owner + err = ctrl.SetControllerReference(parent, res.Rendered, r.Scheme()) + if err != nil { + logger.Error(err, "cannot set owner for namespaced template", "templateName", tmpl.TemplateName) + errorCnt++ + } else { + ns := parent.GetNamespace() + err = patchUnstructured(ctx, r, res.Rendered, res.GVR, &ns) + if err != nil { + logger.Error(err, "cannot apply namespaced template", "templateName", tmpl.TemplateName) + errorCnt++ + } + } } else { - ns := parent.GetNamespace() - err = patchUnstructured(ctx, r, tmplRes.Resource.Rendered, tmplRes.Resource.GVR, &ns) + err = patchUnstructured(ctx, r, res.Rendered, res.GVR, nil) if err != nil { - logger.Error(err, "cannot apply namespaced template", "templateName", tmplRes.TemplateName) + logger.Error(err, "cannot apply cluster-scoped template", "templateName", tmpl.TemplateName) errorCnt++ } } - } else { - err = patchUnstructured(ctx, r, tmplRes.Resource.Rendered, tmplRes.Resource.GVR, nil) - if err != nil { - logger.Error(err, "cannot apply cluster-scoped template", "templateName", tmplRes.TemplateName) - errorCnt++ - } } } @@ -305,32 +302,20 @@ func template2maps(tmpl *template.Template, tmplValues *TemplateValues) ([]map[s return resources, nil } -// FIXME delete replaced by template2Composite -// func template2Unstructured(tmpl *template.Template, tmplValues *TemplateValues) ([]unstructured.Unstructured, error) { -// rawResources, err := template2maps(tmpl, tmplValues) -// if err != nil { -// return nil, err -// } -// uu := make([]unstructured.Unstructured, 0, len(rawResources)) -// for _, r := range rawResources { -// uu = append(uu, unstructured.Unstructured{Object: r}) -// } -// return uu, nil -// } - func template2Composite(r ControllerClient, tmpl *template.Template, tmplValues *TemplateValues) ([]ResourceComposite, error) { rawResources, err := template2maps(tmpl, tmplValues) if err != nil { return nil, err } composites := make([]ResourceComposite, 0, len(rawResources)) - for _, r := range rawResources { + for rawIdx := range rawResources { c := ResourceComposite{} - c.Rendered = &unstructured.Unstructured{Object: r} + c.Rendered = &unstructured.Unstructured{Object: rawResources[rawIdx]} c.GVR, c.IsNamespaced, err = unstructuredToGVR(r, c.Rendered) if err != nil { return nil, err } + composites = append(composites, c) } return composites, nil } diff --git a/controllers/templating_test.go b/controllers/templating_test.go index 914d7238..7c7291d8 100644 --- a/controllers/templating_test.go +++ b/controllers/templating_test.go @@ -58,8 +58,8 @@ func TestParseTemplate(t *testing.T) { if tmpl == nil || err != nil { t.Fatalf("Error parsing templates %v", err) } - if len(tmpl) != 2 { - t.Fatalf("Template slice lenght mismatch, got %v, expected 2", len(tmpl)) + if len(tmpl) != 3 { + t.Fatalf("Template slice lenght mismatch, got %v, expected 3", len(tmpl)) } if tmpl[0].TemplateName != "t1" { t.Fatalf("Template[0] name, got %v, expected t1", tmpl[0].TemplateName) @@ -94,26 +94,3 @@ func TestTemplate2map(t *testing.T) { t.Fatalf("Error rendering multi-resource, got len %v, expected 3", len(rawResources)) } } - -func TestTemplate2Unstructured(t *testing.T) { - tmpl, err := helperGetResourceState() - tmplValues := helperGetValues() - u, err := template2Unstructured(tmpl[0].Template, tmplValues) - if u == nil { - t.Fatalf("Cannot render template to map: %v", err) - } - u, err = template2Unstructured(tmpl[1].Template, tmplValues) - if err != nil { - t.Fatalf("Error rendering empty resource, got err %v", err) - } - if len(u) != 0 { - t.Fatalf("Error rendering empty resource, got %v, expected 0", len(u)) - } - u, err = template2Unstructured(tmpl[2].Template, tmplValues) - if err != nil { - t.Fatalf("Error rendering multi-resource, got err %v", err) - } - if len(u) != 3 { - t.Fatalf("Error rendering multi-resource, got len %v, expected 3", len(u)) - } -} From c042d12c84b1e2f5ca1602a5f6db7e3341da4b8b Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Mon, 22 May 2023 15:29:03 +0200 Subject: [PATCH 5/9] Lint fixes and e2e test --- .../gatewayclassblueprint-aws-alb-crossplane.yaml | 4 ++-- .../gatewayclassblueprint-contour-istio-cert.yaml | 2 +- controllers/templating.go | 7 +++---- controllers/templating_test.go | 10 +++++++--- test/e2e/controller_self_test.go | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/blueprints/aws-alb-crossplane/gatewayclassblueprint-aws-alb-crossplane.yaml b/blueprints/aws-alb-crossplane/gatewayclassblueprint-aws-alb-crossplane.yaml index 82edb5b9..8622fcc5 100644 --- a/blueprints/aws-alb-crossplane/gatewayclassblueprint-aws-alb-crossplane.yaml +++ b/blueprints/aws-alb-crossplane/gatewayclassblueprint-aws-alb-crossplane.yaml @@ -33,7 +33,7 @@ spec: template: | addresses: - type: Hostname - value: {{ .Resources.LB.status.atProvider.dnsName }} + value: {{ (index .Resources.LB 0).status.atProvider.dnsName }} resourceTemplates: childGateway: | apiVersion: gateway.networking.k8s.io/v1beta1 @@ -153,7 +153,7 @@ spec: {{- toYaml .Values.tags | nindent 4 }} {{ end }} spec: - targetGroupARN: {{ .Resources.LBTargetGroup.status.atProvider.arn }} + targetGroupARN: {{ (index .Resources.LBTargetGroup 0).status.atProvider.arn }} targetType: ip serviceRef: name: {{ .Gateway.metadata.name }}-child diff --git a/blueprints/contour-istio/gatewayclassblueprint-contour-istio-cert.yaml b/blueprints/contour-istio/gatewayclassblueprint-contour-istio-cert.yaml index 76311044..d7f5342a 100644 --- a/blueprints/contour-istio/gatewayclassblueprint-contour-istio-cert.yaml +++ b/blueprints/contour-istio/gatewayclassblueprint-contour-istio-cert.yaml @@ -9,7 +9,7 @@ spec: status: template: | addresses: - {{ range .Resources.loadBalancer.status.loadBalancer.ingress }} + {{ range (index .Resources.loadBalancer 0).status.loadBalancer.ingress }} - type: IPAddress value: {{ .ip }} {{ end }} diff --git a/controllers/templating.go b/controllers/templating.go index 89b9ca21..539135c1 100644 --- a/controllers/templating.go +++ b/controllers/templating.go @@ -73,15 +73,14 @@ type ResourceTemplateState struct { // Compiled template Template *template.Template - // Resource information, rendered and current - OldResource ResourceComposite // FIXME, refactoring - delete and replace with below - NewResources []ResourceComposite // FIXME, refactoring temp name - // Name of template (from template key in GatewayClassBlueprint, not Kubernetes resource name) TemplateName string // Raw template StringTemplate string + + // Resource information, rendered and current + NewResources []ResourceComposite // FIXME, refactoring temp name } // Parameters used when rendering templates diff --git a/controllers/templating_test.go b/controllers/templating_test.go index 7c7291d8..3843c0c1 100644 --- a/controllers/templating_test.go +++ b/controllers/templating_test.go @@ -1,8 +1,9 @@ package controllers import ( - "k8s.io/apimachinery/pkg/util/yaml" "testing" + + "k8s.io/apimachinery/pkg/util/yaml" ) func TestParseSingleTemplate(t *testing.T) { @@ -59,7 +60,7 @@ func TestParseTemplate(t *testing.T) { t.Fatalf("Error parsing templates %v", err) } if len(tmpl) != 3 { - t.Fatalf("Template slice lenght mismatch, got %v, expected 3", len(tmpl)) + t.Fatalf("Template slice length mismatch, got %v, expected 3", len(tmpl)) } if tmpl[0].TemplateName != "t1" { t.Fatalf("Template[0] name, got %v, expected t1", tmpl[0].TemplateName) @@ -68,9 +69,12 @@ func TestParseTemplate(t *testing.T) { func TestTemplate2map(t *testing.T) { tmpl, err := helperGetResourceState() + if err != nil { + t.Fatalf("Cannot get resource state: %v", err) + } tmplValues := helperGetValues() rawResources, err := template2maps(tmpl[0].Template, tmplValues) - if rawResources == nil { + if rawResources == nil || err != nil { t.Fatalf("Cannot render template to map: %v", err) } if len(rawResources) != 1 { diff --git a/test/e2e/controller_self_test.go b/test/e2e/controller_self_test.go index d4d4a6e6..7b6ac43a 100644 --- a/test/e2e/controller_self_test.go +++ b/test/e2e/controller_self_test.go @@ -79,7 +79,7 @@ spec: template: | addresses: - type: IPAddress - value: {{ .Resources.configMapTestSource.data.testIPAddress }} + value: {{ (index .Resources.configMapTestSource 0).data.testIPAddress }} resourceTemplates: childGateway: | apiVersion: gateway.networking.k8s.io/v1beta1 From 570cc396b9829bc92bf08789fbb7d95c0207470d Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Tue, 23 May 2023 07:24:02 +0200 Subject: [PATCH 6/9] doc: update creating gatewayclass doc --- doc/creating-gatewayclass-definitions.md | 30 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/doc/creating-gatewayclass-definitions.md b/doc/creating-gatewayclass-definitions.md index 20344230..de20c2f2 100644 --- a/doc/creating-gatewayclass-definitions.md +++ b/doc/creating-gatewayclass-definitions.md @@ -78,6 +78,25 @@ includes support for the 100+ functions from the [Sprig library](http://masterminds.github.io/sprig) as well as a `toYaml` function. +Typically templates will result in a single resource, but conditionals +and loops may result in templates rendering to zero or more than one +resource. This is supported, but should be used wit caution. + +Consideration for multi-resource templates: + +- Resources should be separated by a line with `---` (like in Helm). + +- The template as a whole is single unit in the graph of resources, + i.e. individual resources in a template cannot refer to each other + using the `.Resources` method described below. References across + templates using multiple resources are supported. + +- It is supported to mix resource kinds in a single template, however, + consider if it would be more appropriate to use separate templates + in such cases. + +## Namespaced Resources + Namespace-scoped templated resources are always created in the namespace of the parent resource, e.g. a resource defined under `gatewayTemplate` will be created in the namespace of the parent @@ -95,12 +114,17 @@ When a resource template can be rendered without missing references, the rendered template will be used to retrieve the current version of the resource from the API server. These 'current resources' will be made available as template variables under `.Resources` and the name -of the template. +of the template. **Since a template may render to more than one +resource, the `.Resources` variable is a list**. The following excerpt from a `GatewayClassBlueprint` illustrates how a value is read from the status field of one resource `LBTargetGroup` and how the `status.atProvider.arn` value is used in the template of -`TargetGroupBinding` through `.Resources.LBTargetGroup`. +`TargetGroupBinding` through `.Resources.LBTargetGroup`. The use of +the `index` function is because we refer to the first resource +rendered from the `LBTargetGroup` template. This is necessary even if +we in this case know that there is always only a single resource +rendered from the template. ```yaml ... @@ -120,7 +144,7 @@ spec: ... spec: # And here we use the value 'status.atProvider.arn' from the 'LBTargetGroup' resource - targetGroupARN: {{ .Resources.LBTargetGroup.status.atProvider.arn }} + targetGroupARN: {{ (index .Resources.LBTargetGroup 0).status.atProvider.arn }} ``` The following figure illustrates variables available to templates, From c6104a3bab4e6346e06b57d04123dbe6474b71d7 Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Tue, 23 May 2023 11:55:26 +0200 Subject: [PATCH 7/9] test: add test using range in template --- controllers/common_test.go | 2 +- controllers/gateway_controller_test.go | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/controllers/common_test.go b/controllers/common_test.go index 09575f41..01f29b73 100644 --- a/controllers/common_test.go +++ b/controllers/common_test.go @@ -173,7 +173,7 @@ spec: name: default ` -var _ = Describe("Common functions", func() { +var _ = Describe("Attached policies and value precedence", func() { const ( timeout = time.Second * 10 diff --git a/controllers/gateway_controller_test.go b/controllers/gateway_controller_test.go index 3b729d0c..dc7d048f 100644 --- a/controllers/gateway_controller_test.go +++ b/controllers/gateway_controller_test.go @@ -81,6 +81,12 @@ kind: GatewayClassBlueprint metadata: name: default-gateway-class spec: + values: + default: + configmap2SuffixData: + - one + - two + - three gatewayTemplate: status: template: | @@ -109,14 +115,25 @@ spec: data: valueToRead1: Hello valueToRead2: World - configMapTestIntermediate: | + configMapTestIntermediate1: | apiVersion: v1 kind: ConfigMap metadata: - name: intermediate-configmap + name: intermediate1-configmap namespace: {{ .Gateway.metadata.namespace }} data: valueIntermediate: {{ (index .Resources.configMapTestSource 0).data.valueToRead1 }} + configMapTestIntermediate2: | + {{ range .Values.configmap2SuffixData }} + apiVersion: v1 + kind: ConfigMap + metadata: + name: intermediate2-configmap-{{ . }} + namespace: {{ $.Gateway.metadata.namespace }} + data: + valueIntermediate: {{ (index $.Resources.configMapTestSource 0).data.valueToRead1 }}-{{ . }} + --- + {{ end }} # Use references to multiple resources coupled with template pipeline and functions configMapTestDestination: | apiVersion: v1 @@ -125,7 +142,8 @@ spec: name: dst-configmap namespace: {{ .Gateway.metadata.namespace }} data: - valueRead: {{ printf "%s, %s" (index .Resources.configMapTestIntermediate 0).data.valueIntermediate (index .Resources.configMapTestSource 0).data.valueToRead2 | upper }} + valueRead: {{ printf "%s, %s" (index .Resources.configMapTestIntermediate1 0).data.valueIntermediate (index .Resources.configMapTestSource 0).data.valueToRead2 | upper }} + valueRead2: {{ printf "Testing, one two %s" (index .Resources.configMapTestIntermediate2 2).data.valueIntermediate | upper }} httpRouteTemplate: resourceTemplates: shadowHttproute: | @@ -270,6 +288,7 @@ var _ = Describe("Gateway controller", func() { By("Setting the content of the destination configmap") Expect(cm.Data["valueRead"]).To(Equal("HELLO, WORLD")) + Expect(cm.Data["valueRead2"]).To(Equal("TESTING, ONE TWO HELLO-THREE")) }) }) }) From e0c492f764a39ab11c023799304d5c5358f2e32f Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Tue, 23 May 2023 13:12:28 +0200 Subject: [PATCH 8/9] Update tests to use range with both data and index --- controllers/gateway_controller_test.go | 6 +++--- controllers/templating_test.go | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/controllers/gateway_controller_test.go b/controllers/gateway_controller_test.go index dc7d048f..c171e347 100644 --- a/controllers/gateway_controller_test.go +++ b/controllers/gateway_controller_test.go @@ -124,14 +124,14 @@ spec: data: valueIntermediate: {{ (index .Resources.configMapTestSource 0).data.valueToRead1 }} configMapTestIntermediate2: | - {{ range .Values.configmap2SuffixData }} + {{ range $idx,$suffix := .Values.configmap2SuffixData }} apiVersion: v1 kind: ConfigMap metadata: - name: intermediate2-configmap-{{ . }} + name: intermediate2-configmap-{{ $idx }} namespace: {{ $.Gateway.metadata.namespace }} data: - valueIntermediate: {{ (index $.Resources.configMapTestSource 0).data.valueToRead1 }}-{{ . }} + valueIntermediate: {{ (index $.Resources.configMapTestSource 0).data.valueToRead1 }}-{{ $suffix }} --- {{ end }} # Use references to multiple resources coupled with template pipeline and functions diff --git a/controllers/templating_test.go b/controllers/templating_test.go index 3843c0c1..88b4ae0a 100644 --- a/controllers/templating_test.go +++ b/controllers/templating_test.go @@ -22,8 +22,8 @@ t2: | name: {{ .Values.name2 }} {{ end }} t3: | - {{ range .Values.t3data }} - name: {{ $.Values.name3 }}-{{ . }} + {{ range $idx,$data := .Values.t3data }} + name: {{ $.Values.name3 }}-{{ $data }}-{{ $idx }} --- {{ end }} ` @@ -97,4 +97,7 @@ func TestTemplate2map(t *testing.T) { if len(rawResources) != 3 { t.Fatalf("Error rendering multi-resource, got len %v, expected 3", len(rawResources)) } + if rawResources[2]["name"] != "t3name-foo3-2" { + t.Fatalf("Rendered template error, got %v, expected 't3name-foo3-2'", rawResources[2]["name"]) + } } From 011747365d1a3ecf0fd24160f7845acb08ea2836 Mon Sep 17 00:00:00 2001 From: Michael Vittrup Larsen Date: Thu, 25 May 2023 08:56:43 +0200 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Martin Villumsen --- doc/creating-gatewayclass-definitions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/creating-gatewayclass-definitions.md b/doc/creating-gatewayclass-definitions.md index de20c2f2..2a3bc371 100644 --- a/doc/creating-gatewayclass-definitions.md +++ b/doc/creating-gatewayclass-definitions.md @@ -80,13 +80,13 @@ function. Typically templates will result in a single resource, but conditionals and loops may result in templates rendering to zero or more than one -resource. This is supported, but should be used wit caution. +resource. This is supported but should be used with caution. Consideration for multi-resource templates: - Resources should be separated by a line with `---` (like in Helm). -- The template as a whole is single unit in the graph of resources, +- The template as a whole is a single unit in the graph of resources, i.e. individual resources in a template cannot refer to each other using the `.Resources` method described below. References across templates using multiple resources are supported.