From d889e78ac0820283424d222433f37e883f7438a9 Mon Sep 17 00:00:00 2001 From: Chris Doherty Date: Fri, 5 Aug 2022 11:13:12 -0400 Subject: [PATCH] Expose custom funcs to template Additional helper functions have proven useful to consumers when rendering templates with hardware data. The concrete use-case is formatting of partitions where nvme uses a different format to the historical block device. Signed-off-by: Chris Doherty --- pkg/controllers/workflow/controller.go | 15 +- workflow/funcs.go | 30 +++ workflow/template_validator.go | 8 +- workflow/template_validator_test.go | 345 +++++++++++++++++++++++++ 4 files changed, 387 insertions(+), 11 deletions(-) create mode 100644 workflow/funcs.go diff --git a/pkg/controllers/workflow/controller.go b/pkg/controllers/workflow/controller.go index 478ac76a6..99e0135b2 100644 --- a/pkg/controllers/workflow/controller.go +++ b/pkg/controllers/workflow/controller.go @@ -90,12 +90,11 @@ func (c *Controller) processNewWorkflow(ctx context.Context, logger logr.Logger, } data := make(map[string]interface{}) - for key, val := range stored.Spec.HardwareMap { data[key] = val } - var hardware v1alpha1.Hardware + var hardware v1alpha1.Hardware err := c.kubeClient.Get(ctx, client.ObjectKey{Name: stored.Spec.HardwareRef, Namespace: stored.Namespace}, &hardware) if err != nil && !errors.IsNotFound(err) { logger.Error(err, "error getting Hardware object in processNewWorkflow function") @@ -112,8 +111,7 @@ func (c *Controller) processNewWorkflow(ctx context.Context, logger logr.Logger, } if err == nil { - // convert between hardware and hardwareTemplate type - contract := toTemplateHardware(hardware) + contract := toTemplateHardwareData(hardware) data["Hardware"] = contract } @@ -129,12 +127,15 @@ func (c *Controller) processNewWorkflow(ctx context.Context, logger logr.Logger, return reconcile.Result{}, nil } -type hardwareTemplate struct { +// templateHardwareData defines the data exposed for a Hardware instance to a Template. +type templateHardwareData struct { Disks []string } -func toTemplateHardware(hardware v1alpha1.Hardware) hardwareTemplate { - var contract hardwareTemplate +// toTemplateHardwareData converts a Hardware instance of templateHardwareData for use in template +// rendering. +func toTemplateHardwareData(hardware v1alpha1.Hardware) templateHardwareData { + var contract templateHardwareData for _, disk := range hardware.Spec.Disks { contract.Disks = append(contract.Disks, disk.Device) } diff --git a/workflow/funcs.go b/workflow/funcs.go new file mode 100644 index 000000000..2b4f1f456 --- /dev/null +++ b/workflow/funcs.go @@ -0,0 +1,30 @@ +package workflow + +import ( + "fmt" + "strings" +) + +// templateFuncs defines the custom functions available to workflow templates. +var templateFuncs = map[string]interface{}{ + "contains": strings.Contains, + "hasPrefix": strings.HasPrefix, + "hasSuffix": strings.HasSuffix, + "formatPartition": formatPartition, +} + +// formatPartition formats a device path with partition for the device type. If it receives an +// unidentifiable device path it returns the dev. +// +// Examples +// formatPartition("/dev/nvme0n1", 0) -> /dev/nvme0n1p1 +// formatPartition("/dev/sda", 1) -> /dev/sda1 +func formatPartition(dev string, partition int) string { + switch { + case strings.HasPrefix(dev, "/dev/nvme"): + return fmt.Sprintf("%vp%v", dev, partition) + case strings.HasPrefix(dev, "/dev/sd"): + return fmt.Sprintf("%v%v", dev, partition) + } + return dev +} diff --git a/workflow/template_validator.go b/workflow/template_validator.go index 1d019660b..aa0f3c561 100644 --- a/workflow/template_validator.go +++ b/workflow/template_validator.go @@ -78,17 +78,17 @@ func RenderTemplate(templateID, templateData string, devices []byte) (string, er // RenderTemplateHardware renders the workflow template and returns the Workflow and the interpolated bytes. func RenderTemplateHardware(templateID, templateData string, hardware map[string]interface{}) (*Workflow, *bytes.Buffer, error) { - t := template.New("workflow-template").Option("missingkey=error") + t := template.New("workflow-template"). + Option("missingkey=error"). + Funcs(templateFuncs) _, err := t.Parse(templateData) if err != nil { err = errors.Wrapf(err, errTemplateParsing, templateID) return nil, nil, err } - // introduces hardware to the template rendering buf := new(bytes.Buffer) - err = t.Execute(buf, hardware) - if err != nil { + if err = t.Execute(buf, hardware); err != nil { err = errors.Wrapf(err, errTemplateParsing, templateID) return nil, nil, err } diff --git a/workflow/template_validator_test.go b/workflow/template_validator_test.go index 9d4b52c07..63b9c6948 100644 --- a/workflow/template_validator_test.go +++ b/workflow/template_validator_test.go @@ -421,6 +421,351 @@ tasks: } } +func TestRenderTemplateHardwareCustomFuncs(t *testing.T) { + cases := []struct { + name string + hwAddress []byte + templateID string + templateData string + expected Workflow + }{ + { + name: "contains/isFalse", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test{{ if ( contains "foo" "bar" ) }}test{{ end }}" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "test", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + }, + }, + }, + }, + }, + }, + { + name: "contains/isTrue", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test{{ if ( contains "foo" "foo" ) }}test{{ end }}" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "testtest", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + }, + }, + }, + }, + }, + }, + { + name: "hasPrefix/isFalse", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test{{ if ( hasPrefix "foo" "bar" ) }}test{{ end }}" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "test", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + }, + }, + }, + }, + }, + }, + { + name: "hasPrefix/isTrue", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test{{ if ( hasPrefix "foo" "foo" ) }}test{{ end }}" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "testtest", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + }, + }, + }, + }, + }, + }, + { + name: "hasSuffix/isFalse", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test{{ if ( hasSuffix "foo" "bar" ) }}test{{ end }}" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "test", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + }, + }, + }, + }, + }, + }, + { + name: "hasSuffix/isTrue", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test{{ if ( hasSuffix "foo" "foo" ) }}test{{ end }}" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "testtest", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + }, + }, + }, + }, + }, + }, + { + name: "formatPartition/block device", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 + environment: + DEST_DISK: {{ formatPartition "/dev/sda" 1 }} +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "test", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + Environment: map[string]string{ + "DEST_DISK": "/dev/sda1", + }, + }, + }, + }, + }, + }, + }, + { + name: "formatPartition/nvme device", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 + environment: + DEST_DISK: {{ formatPartition "/dev/nvme0n1" 1 }} +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "test", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + Environment: map[string]string{ + "DEST_DISK": "/dev/nvme0n1p1", + }, + }, + }, + }, + }, + }, + }, + { + name: "formatPartition/unknown", + templateID: "test", + templateData: ` +version: "0.1" +name: test +global_timeout: 1 +tasks: + - name: "test" + worker: "test" + actions: + - name: "test" + image: test + timeout: 60 + environment: + DEST_DISK: {{ formatPartition "/dev/foobar" 1 }} +`, + expected: Workflow{ + Name: "test", + GlobalTimeout: 1, + Version: "0.1", + Tasks: []Task{ + { + Name: "test", + WorkerAddr: "test", + Actions: []Action{ + { + Name: "test", + Image: "test", + Timeout: 60, + Environment: map[string]string{ + "DEST_DISK": "/dev/foobar", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + wflw, _, err := RenderTemplateHardware(tc.templateID, tc.templateData, map[string]interface{}{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if diff := cmp.Diff(*wflw, tc.expected); diff != "" { + t.Errorf("unexpected workflow (-want +got):\n%s", diff) + } + }) + } +} + type workflowModifier func(*Workflow) func workflow(m ...workflowModifier) *Workflow {