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  }