github.com/kubeshop/testkube@v1.17.23/pkg/tcl/testworkflowstcl/testworkflowprocessor/processor.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 testworkflowprocessor
    10  
    11  import (
    12  	"context"
    13  	"encoding/json"
    14  	"fmt"
    15  	"maps"
    16  	"path/filepath"
    17  
    18  	"github.com/pkg/errors"
    19  	batchv1 "k8s.io/api/batch/v1"
    20  	corev1 "k8s.io/api/core/v1"
    21  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  
    23  	testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
    24  	"github.com/kubeshop/testkube/internal/common"
    25  	"github.com/kubeshop/testkube/pkg/imageinspector"
    26  	"github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
    27  	"github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor/constants"
    28  )
    29  
    30  //go:generate mockgen -destination=./mock_processor.go -package=testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" Processor
    31  type Processor interface {
    32  	Register(operation Operation) Processor
    33  	Bundle(ctx context.Context, workflow *testworkflowsv1.TestWorkflow, machines ...expressionstcl.Machine) (*Bundle, error)
    34  }
    35  
    36  //go:generate mockgen -destination=./mock_internalprocessor.go -package=testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" InternalProcessor
    37  type InternalProcessor interface {
    38  	Process(layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error)
    39  }
    40  
    41  type Operation = func(processor InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error)
    42  
    43  type processor struct {
    44  	inspector  imageinspector.Inspector
    45  	operations []Operation
    46  }
    47  
    48  func New(inspector imageinspector.Inspector) Processor {
    49  	return &processor{inspector: inspector}
    50  }
    51  
    52  func NewFullFeatured(inspector imageinspector.Inspector) Processor {
    53  	return New(inspector).
    54  		Register(ProcessDelay).
    55  		Register(ProcessContentFiles).
    56  		Register(ProcessContentGit).
    57  		Register(ProcessNestedSetupSteps).
    58  		Register(ProcessRunCommand).
    59  		Register(ProcessShellCommand).
    60  		Register(ProcessExecute).
    61  		Register(ProcessNestedSteps).
    62  		Register(ProcessArtifacts)
    63  }
    64  
    65  func (p *processor) Register(operation Operation) Processor {
    66  	p.operations = append(p.operations, operation)
    67  	return p
    68  }
    69  
    70  func (p *processor) process(layer Intermediate, container Container, step testworkflowsv1.Step, ref string) (Stage, error) {
    71  	// Configure defaults
    72  	if step.WorkingDir != nil {
    73  		container.SetWorkingDir(*step.WorkingDir)
    74  	}
    75  	container.ApplyCR(step.Container)
    76  
    77  	// Build an initial group for the inner items
    78  	self := NewGroupStage(ref, false)
    79  	self.SetName(step.Name)
    80  	self.SetOptional(step.Optional).SetNegative(step.Negative).SetTimeout(step.Timeout)
    81  	if step.Condition != "" {
    82  		self.SetCondition(step.Condition)
    83  	} else {
    84  		self.SetCondition("passed")
    85  	}
    86  
    87  	// Run operations
    88  	for _, op := range p.operations {
    89  		stage, err := op(p, layer, container, step)
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  		self.Add(stage)
    94  	}
    95  
    96  	return self, nil
    97  }
    98  
    99  func (p *processor) Process(layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
   100  	return p.process(layer, container, step, layer.NextRef())
   101  }
   102  
   103  func (p *processor) Bundle(ctx context.Context, workflow *testworkflowsv1.TestWorkflow, machines ...expressionstcl.Machine) (bundle *Bundle, err error) {
   104  	// Initialize intermediate layer
   105  	layer := NewIntermediate().
   106  		AppendPodConfig(workflow.Spec.Pod).
   107  		AppendJobConfig(workflow.Spec.Job)
   108  	layer.ContainerDefaults().
   109  		ApplyCR(constants.DefaultContainerConfig.DeepCopy()).
   110  		AppendVolumeMounts(layer.AddEmptyDirVolume(nil, constants.DefaultInternalPath)).
   111  		AppendVolumeMounts(layer.AddEmptyDirVolume(nil, constants.DefaultDataPath))
   112  
   113  	// Process steps
   114  	rootStep := testworkflowsv1.Step{
   115  		StepBase: testworkflowsv1.StepBase{
   116  			Content:   workflow.Spec.Content,
   117  			Container: workflow.Spec.Container,
   118  		},
   119  		Steps: append(workflow.Spec.Setup, append(workflow.Spec.Steps, workflow.Spec.After...)...),
   120  	}
   121  	err = expressionstcl.Simplify(&workflow, machines...)
   122  	if err != nil {
   123  		return nil, errors.Wrap(err, "error while simplifying workflow instructions")
   124  	}
   125  	root, err := p.process(layer, layer.ContainerDefaults(), rootStep, "")
   126  	if err != nil {
   127  		return nil, errors.Wrap(err, "processing error")
   128  	}
   129  
   130  	// Validate if there is anything to run
   131  	if root.Len() == 0 {
   132  		return nil, errors.New("test workflow has nothing to run")
   133  	}
   134  
   135  	// Finalize ConfigMaps
   136  	configMaps := layer.ConfigMaps()
   137  	for i := range configMaps {
   138  		AnnotateControlledBy(&configMaps[i], "{{execution.id}}")
   139  		err = expressionstcl.FinalizeForce(&configMaps[i], machines...)
   140  		if err != nil {
   141  			return nil, errors.Wrap(err, "finalizing ConfigMap")
   142  		}
   143  	}
   144  
   145  	// Finalize Secrets
   146  	secrets := layer.Secrets()
   147  	for i := range secrets {
   148  		AnnotateControlledBy(&secrets[i], "{{execution.id}}")
   149  		err = expressionstcl.FinalizeForce(&secrets[i], machines...)
   150  		if err != nil {
   151  			return nil, errors.Wrap(err, "finalizing Secret")
   152  		}
   153  	}
   154  
   155  	// Finalize Volumes
   156  	volumes := layer.Volumes()
   157  	for i := range volumes {
   158  		err = expressionstcl.FinalizeForce(&volumes[i], machines...)
   159  		if err != nil {
   160  			return nil, errors.Wrap(err, "finalizing Volume")
   161  		}
   162  	}
   163  
   164  	// Append main label for the pod
   165  	layer.AppendPodConfig(&testworkflowsv1.PodConfig{
   166  		Labels: map[string]string{
   167  			constants.ExecutionIdMainPodLabelName: "{{execution.id}}",
   168  		},
   169  	})
   170  
   171  	// Resolve job & pod config
   172  	jobConfig, podConfig := layer.JobConfig(), layer.PodConfig()
   173  	err = expressionstcl.FinalizeForce(&jobConfig, machines...)
   174  	if err != nil {
   175  		return nil, errors.Wrap(err, "finalizing job config")
   176  	}
   177  	err = expressionstcl.FinalizeForce(&podConfig, machines...)
   178  	if err != nil {
   179  		return nil, errors.Wrap(err, "finalizing pod config")
   180  	}
   181  
   182  	// Build signature
   183  	sig := root.Signature().Children()
   184  
   185  	// Load the image pull secrets
   186  	pullSecretNames := make([]string, len(podConfig.ImagePullSecrets))
   187  	for i, v := range podConfig.ImagePullSecrets {
   188  		pullSecretNames[i] = v.Name
   189  	}
   190  
   191  	// Load the image details
   192  	imageNames := root.GetImages()
   193  	images := make(map[string]*imageinspector.Info)
   194  	for image := range imageNames {
   195  		info, err := p.inspector.Inspect(ctx, "", image, corev1.PullIfNotPresent, pullSecretNames)
   196  		if err != nil {
   197  			return nil, fmt.Errorf("resolving image error: %s: %s", image, err.Error())
   198  		}
   199  		images[image] = info
   200  	}
   201  	err = root.ApplyImages(images)
   202  	if err != nil {
   203  		return nil, errors.Wrap(err, "applying image data")
   204  	}
   205  
   206  	// Build list of the containers
   207  	containers, err := buildKubernetesContainers(root, NewInitProcess().SetRef(root.Ref()), machines...)
   208  	if err != nil {
   209  		return nil, errors.Wrap(err, "building Kubernetes containers")
   210  	}
   211  	for i := range containers {
   212  		err = expressionstcl.FinalizeForce(&containers[i].EnvFrom, machines...)
   213  		if err != nil {
   214  			return nil, errors.Wrap(err, "finalizing container's envFrom")
   215  		}
   216  		err = expressionstcl.FinalizeForce(&containers[i].VolumeMounts, machines...)
   217  		if err != nil {
   218  			return nil, errors.Wrap(err, "finalizing container's volumeMounts")
   219  		}
   220  		err = expressionstcl.FinalizeForce(&containers[i].Resources, machines...)
   221  		if err != nil {
   222  			return nil, errors.Wrap(err, "finalizing container's resources")
   223  		}
   224  
   225  		// Resolve relative paths in the volumeMounts relatively to the working dir
   226  		workingDir := constants.DefaultDataPath
   227  		if containers[i].WorkingDir != "" {
   228  			workingDir = containers[i].WorkingDir
   229  		}
   230  		for j := range containers[i].VolumeMounts {
   231  			if !filepath.IsAbs(containers[i].VolumeMounts[j].MountPath) {
   232  				containers[i].VolumeMounts[j].MountPath = filepath.Clean(filepath.Join(workingDir, containers[i].VolumeMounts[j].MountPath))
   233  			}
   234  		}
   235  	}
   236  
   237  	// Build pod template
   238  	podSpec := corev1.PodTemplateSpec{
   239  		ObjectMeta: metav1.ObjectMeta{
   240  			Annotations: podConfig.Annotations,
   241  			Labels:      podConfig.Labels,
   242  		},
   243  		Spec: corev1.PodSpec{
   244  			RestartPolicy:      corev1.RestartPolicyNever,
   245  			Volumes:            volumes,
   246  			ImagePullSecrets:   podConfig.ImagePullSecrets,
   247  			ServiceAccountName: podConfig.ServiceAccountName,
   248  			NodeSelector:       podConfig.NodeSelector,
   249  			SecurityContext: &corev1.PodSecurityContext{
   250  				FSGroup: common.Ptr(constants.DefaultFsGroup),
   251  			},
   252  		},
   253  	}
   254  	AnnotateControlledBy(&podSpec, "{{execution.id}}")
   255  	err = expressionstcl.FinalizeForce(&podSpec, machines...)
   256  	if err != nil {
   257  		return nil, errors.Wrap(err, "finalizing pod template spec")
   258  	}
   259  	initContainer := corev1.Container{
   260  		Name:            "tktw-init",
   261  		Image:           constants.DefaultInitImage,
   262  		ImagePullPolicy: corev1.PullIfNotPresent,
   263  		Command:         []string{"/bin/sh", "-c"},
   264  		Args:            []string{constants.InitScript},
   265  		VolumeMounts:    layer.ContainerDefaults().VolumeMounts(),
   266  		SecurityContext: &corev1.SecurityContext{
   267  			RunAsGroup: common.Ptr(constants.DefaultFsGroup),
   268  		},
   269  	}
   270  	err = expressionstcl.FinalizeForce(&initContainer, machines...)
   271  	if err != nil {
   272  		return nil, errors.Wrap(err, "finalizing container's resources")
   273  	}
   274  	podSpec.Spec.InitContainers = append([]corev1.Container{initContainer}, containers[:len(containers)-1]...)
   275  	podSpec.Spec.Containers = containers[len(containers)-1:]
   276  
   277  	// Build job spec
   278  	jobSpec := batchv1.Job{
   279  		TypeMeta: metav1.TypeMeta{
   280  			Kind:       "Job",
   281  			APIVersion: batchv1.SchemeGroupVersion.String(),
   282  		},
   283  		ObjectMeta: metav1.ObjectMeta{
   284  			Name:        "{{execution.id}}",
   285  			Annotations: jobConfig.Annotations,
   286  			Labels:      jobConfig.Labels,
   287  		},
   288  		Spec: batchv1.JobSpec{
   289  			BackoffLimit: common.Ptr(int32(0)),
   290  		},
   291  	}
   292  	AnnotateControlledBy(&jobSpec, "{{execution.id}}")
   293  	err = expressionstcl.FinalizeForce(&jobSpec, machines...)
   294  	if err != nil {
   295  		return nil, errors.Wrap(err, "finalizing job spec")
   296  	}
   297  	jobSpec.Spec.Template = podSpec
   298  
   299  	// Build signature
   300  	sigSerialized, _ := json.Marshal(sig)
   301  	jobAnnotations := make(map[string]string)
   302  	maps.Copy(jobAnnotations, jobSpec.Annotations)
   303  	maps.Copy(jobAnnotations, map[string]string{
   304  		constants.SignatureAnnotationName: string(sigSerialized),
   305  	})
   306  	jobSpec.Annotations = jobAnnotations
   307  
   308  	// Build bundle
   309  	bundle = &Bundle{
   310  		ConfigMaps: configMaps,
   311  		Secrets:    secrets,
   312  		Job:        jobSpec,
   313  		Signature:  sig,
   314  	}
   315  	return bundle, nil
   316  }