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 }