github.com/kubeshop/testkube@v1.17.23/pkg/tcl/testworkflowstcl/testworkflowresolver/apply.go (about) 1 // Copyright 2024 Testkube. 2 // 3 // Licensed as a Testkube Pro file under the Testkube Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt 8 9 package testworkflowresolver 10 11 import ( 12 "fmt" 13 "reflect" 14 15 "github.com/pkg/errors" 16 "k8s.io/apimachinery/pkg/util/intstr" 17 18 testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" 19 "github.com/kubeshop/testkube/internal/common" 20 "github.com/kubeshop/testkube/pkg/rand" 21 "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" 22 ) 23 24 func buildTemplate(template testworkflowsv1.TestWorkflowTemplate, cfg map[string]intstr.IntOrString) (testworkflowsv1.TestWorkflowTemplate, error) { 25 v, err := ApplyWorkflowTemplateConfig(template.DeepCopy(), cfg) 26 if err != nil { 27 return template, err 28 } 29 return *v, err 30 } 31 32 func getTemplate(name string, templates map[string]testworkflowsv1.TestWorkflowTemplate) (tpl testworkflowsv1.TestWorkflowTemplate, err error) { 33 key := GetInternalTemplateName(name) 34 tpl, ok := templates[key] 35 if ok { 36 return tpl, nil 37 } 38 key = GetDisplayTemplateName(key) 39 tpl, ok = templates[key] 40 if ok { 41 return tpl, nil 42 } 43 return tpl, fmt.Errorf(`template "%s" not found`, name) 44 } 45 46 func getConfiguredTemplate(name string, cfg map[string]intstr.IntOrString, templates map[string]testworkflowsv1.TestWorkflowTemplate) (tpl testworkflowsv1.TestWorkflowTemplate, err error) { 47 tpl, err = getTemplate(name, templates) 48 if err != nil { 49 return tpl, err 50 } 51 return buildTemplate(tpl, cfg) 52 } 53 54 func InjectTemplate(workflow *testworkflowsv1.TestWorkflow, template testworkflowsv1.TestWorkflowTemplate) error { 55 if workflow == nil { 56 return nil 57 } 58 // Apply top-level configuration 59 workflow.Spec.Pod = MergePodConfig(template.Spec.Pod, workflow.Spec.Pod) 60 workflow.Spec.Job = MergeJobConfig(template.Spec.Job, workflow.Spec.Job) 61 62 // Apply basic configuration 63 workflow.Spec.Content = MergeContent(template.Spec.Content, workflow.Spec.Content) 64 workflow.Spec.Container = MergeContainerConfig(template.Spec.Container, workflow.Spec.Container) 65 66 // Include the steps from the template 67 setup := common.MapSlice(template.Spec.Setup, ConvertIndependentStepToStep) 68 workflow.Spec.Setup = append(setup, workflow.Spec.Setup...) 69 steps := common.MapSlice(template.Spec.Steps, ConvertIndependentStepToStep) 70 workflow.Spec.Steps = append(steps, workflow.Spec.Steps...) 71 after := common.MapSlice(template.Spec.After, ConvertIndependentStepToStep) 72 workflow.Spec.After = append(workflow.Spec.After, after...) 73 return nil 74 } 75 76 func InjectStepTemplate(step *testworkflowsv1.Step, template testworkflowsv1.TestWorkflowTemplate) error { 77 if step == nil { 78 return nil 79 } 80 81 // Apply basic configuration 82 step.Content = MergeContent(template.Spec.Content, step.Content) 83 step.Container = MergeContainerConfig(template.Spec.Container, step.Container) 84 85 // Fast-track when the template doesn't contain any steps to run 86 if len(template.Spec.Setup) == 0 && len(template.Spec.Steps) == 0 && len(template.Spec.After) == 0 { 87 return nil 88 } 89 90 // Decouple sub-steps from the template 91 setup := common.MapSlice(template.Spec.Setup, ConvertIndependentStepToStep) 92 steps := common.MapSlice(template.Spec.Steps, ConvertIndependentStepToStep) 93 after := common.MapSlice(template.Spec.After, ConvertIndependentStepToStep) 94 95 step.Setup = append(setup, step.Setup...) 96 step.Steps = append(steps, append(step.Steps, after...)...) 97 98 return nil 99 } 100 101 func applyTemplatesToStep(step testworkflowsv1.Step, templates map[string]testworkflowsv1.TestWorkflowTemplate) (testworkflowsv1.Step, error) { 102 // Apply regular templates 103 for i, ref := range step.Use { 104 tpl, err := getConfiguredTemplate(ref.Name, ref.Config, templates) 105 if err != nil { 106 return step, errors.Wrap(err, fmt.Sprintf(".use[%d]: resolving template", i)) 107 } 108 err = InjectStepTemplate(&step, tpl) 109 if err != nil { 110 return step, errors.Wrap(err, fmt.Sprintf(".use[%d]: injecting template", i)) 111 } 112 } 113 step.Use = nil 114 115 // Apply alternative template syntax 116 if step.Template != nil { 117 tpl, err := getConfiguredTemplate(step.Template.Name, step.Template.Config, templates) 118 if err != nil { 119 return step, errors.Wrap(err, ".template: resolving template") 120 } 121 isolate := testworkflowsv1.Step{} 122 err = InjectStepTemplate(&isolate, tpl) 123 if err != nil { 124 return step, errors.Wrap(err, ".template: injecting template") 125 } 126 127 if len(isolate.Setup) > 0 || len(isolate.Steps) > 0 { 128 if isolate.Container == nil && isolate.Content == nil && isolate.WorkingDir == nil { 129 step.Steps = append(append(isolate.Setup, isolate.Steps...), step.Steps...) 130 } else { 131 step.Steps = append([]testworkflowsv1.Step{isolate}, step.Steps...) 132 } 133 } 134 135 step.Template = nil 136 } 137 138 // Resolve templates in the sub-steps 139 var err error 140 for i := range step.Setup { 141 step.Setup[i], err = applyTemplatesToStep(step.Setup[i], templates) 142 if err != nil { 143 return step, errors.Wrap(err, fmt.Sprintf(".steps[%d]", i)) 144 } 145 } 146 for i := range step.Steps { 147 step.Steps[i], err = applyTemplatesToStep(step.Steps[i], templates) 148 if err != nil { 149 return step, errors.Wrap(err, fmt.Sprintf(".steps[%d]", i)) 150 } 151 } 152 153 return step, nil 154 } 155 156 func FlattenStepList(steps []testworkflowsv1.Step) []testworkflowsv1.Step { 157 changed := false 158 result := make([]testworkflowsv1.Step, 0, len(steps)) 159 for _, step := range steps { 160 setup := step.Setup 161 sub := step.Steps 162 step.Setup = nil 163 step.Steps = nil 164 if reflect.ValueOf(step).IsZero() { 165 changed = true 166 result = append(result, append(setup, sub...)...) 167 } else { 168 step.Setup = setup 169 step.Steps = sub 170 result = append(result, step) 171 } 172 } 173 if !changed { 174 return steps 175 } 176 return result 177 } 178 179 func ApplyTemplates(workflow *testworkflowsv1.TestWorkflow, templates map[string]testworkflowsv1.TestWorkflowTemplate) error { 180 if workflow == nil { 181 return nil 182 } 183 184 // Encapsulate TestWorkflow configuration to not pass it into templates accidentally 185 random := rand.String(10) 186 err := expressionstcl.Simplify(workflow, expressionstcl.ReplacePrefixMachine("config.", random+".")) 187 if err != nil { 188 return err 189 } 190 defer expressionstcl.Simplify(workflow, expressionstcl.ReplacePrefixMachine(random+".", "config.")) 191 192 // Apply top-level templates 193 for i, ref := range workflow.Spec.Use { 194 tpl, err := getConfiguredTemplate(ref.Name, ref.Config, templates) 195 if err != nil { 196 return errors.Wrap(err, fmt.Sprintf("spec.use[%d]: resolving template", i)) 197 } 198 err = InjectTemplate(workflow, tpl) 199 if err != nil { 200 return errors.Wrap(err, fmt.Sprintf("spec.use[%d]: injecting template", i)) 201 } 202 } 203 workflow.Spec.Use = nil 204 205 // Apply templates on the step level 206 for i := range workflow.Spec.Setup { 207 workflow.Spec.Setup[i], err = applyTemplatesToStep(workflow.Spec.Setup[i], templates) 208 if err != nil { 209 return errors.Wrap(err, fmt.Sprintf("spec.setup[%d]", i)) 210 } 211 } 212 for i := range workflow.Spec.Steps { 213 workflow.Spec.Steps[i], err = applyTemplatesToStep(workflow.Spec.Steps[i], templates) 214 if err != nil { 215 return errors.Wrap(err, fmt.Sprintf("spec.steps[%d]", i)) 216 } 217 } 218 for i := range workflow.Spec.After { 219 workflow.Spec.After[i], err = applyTemplatesToStep(workflow.Spec.After[i], templates) 220 if err != nil { 221 return errors.Wrap(err, fmt.Sprintf("spec.after[%d]", i)) 222 } 223 } 224 225 // Simplify the lists 226 workflow.Spec.Setup = FlattenStepList(workflow.Spec.Setup) 227 workflow.Spec.Steps = FlattenStepList(workflow.Spec.Steps) 228 workflow.Spec.After = FlattenStepList(workflow.Spec.After) 229 230 return nil 231 }