github.com/kubeshop/testkube@v1.17.23/pkg/tcl/testworkflowstcl/testworkflowprocessor/operations.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 "encoding/json" 13 "fmt" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/pkg/errors" 20 corev1 "k8s.io/api/core/v1" 21 22 testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" 23 "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" 24 "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor/constants" 25 ) 26 27 func ProcessDelay(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 28 if step.Delay == "" { 29 return nil, nil 30 } 31 t, err := time.ParseDuration(step.Delay) 32 if err != nil { 33 return nil, errors.Wrap(err, fmt.Sprintf("invalid duration: %s", step.Delay)) 34 } 35 shell := container.CreateChild(). 36 SetCommand("sleep"). 37 SetArgs(fmt.Sprintf("%g", t.Seconds())) 38 stage := NewContainerStage(layer.NextRef(), shell) 39 stage.SetCategory(fmt.Sprintf("Delay: %s", step.Delay)) 40 return stage, nil 41 } 42 43 func ProcessShellCommand(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 44 if step.Shell == "" { 45 return nil, nil 46 } 47 shell := container.CreateChild().SetCommand(constants.DefaultShellPath).SetArgs("-c", constants.DefaultShellHeader+step.Shell) 48 stage := NewContainerStage(layer.NextRef(), shell) 49 stage.SetCategory("Run shell command") 50 stage.SetRetryPolicy(step.Retry) 51 return stage, nil 52 } 53 54 func ProcessRunCommand(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 55 if step.Run == nil { 56 return nil, nil 57 } 58 container = container.CreateChild().ApplyCR(&step.Run.ContainerConfig) 59 stage := NewContainerStage(layer.NextRef(), container) 60 stage.SetRetryPolicy(step.Retry) 61 stage.SetCategory("Run") 62 if step.Run.Shell != nil { 63 if step.Run.ContainerConfig.Command != nil || step.Run.ContainerConfig.Args != nil { 64 return nil, errors.New("run.shell should not be used in conjunction with run.command or run.args") 65 } 66 stage.SetCategory("Run shell command") 67 stage.Container().SetCommand(constants.DefaultShellPath).SetArgs("-c", constants.DefaultShellHeader+*step.Run.Shell) 68 } 69 return stage, nil 70 } 71 72 func ProcessNestedSetupSteps(p InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 73 group := NewGroupStage(layer.NextRef(), true) 74 for _, n := range step.Setup { 75 stage, err := p.Process(layer, container.CreateChild(), n) 76 if err != nil { 77 return nil, err 78 } 79 group.Add(stage) 80 } 81 return group, nil 82 } 83 84 func ProcessNestedSteps(p InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 85 group := NewGroupStage(layer.NextRef(), true) 86 for _, n := range step.Steps { 87 stage, err := p.Process(layer, container.CreateChild(), n) 88 if err != nil { 89 return nil, err 90 } 91 group.Add(stage) 92 } 93 return group, nil 94 } 95 96 func ProcessExecute(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 97 if step.Execute == nil { 98 return nil, nil 99 } 100 container = container.CreateChild() 101 stage := NewContainerStage(layer.NextRef(), container) 102 stage.SetRetryPolicy(step.Retry) 103 hasWorkflows := len(step.Execute.Workflows) > 0 104 hasTests := len(step.Execute.Tests) > 0 105 106 // Fail if there is nothing to run 107 if !hasTests && !hasWorkflows { 108 return nil, errors.New("no test workflows and tests provided to the 'execute' step") 109 } 110 111 container. 112 SetImage(constants.DefaultToolkitImage). 113 SetImagePullPolicy(corev1.PullIfNotPresent). 114 SetCommand("/toolkit", "execute"). 115 EnableToolkit(stage.Ref()) 116 args := make([]string, 0) 117 for _, t := range step.Execute.Tests { 118 b, err := json.Marshal(t) 119 if err != nil { 120 return nil, errors.Wrap(err, "execute: serializing Test") 121 } 122 args = append(args, "-t", expressionstcl.NewStringValue(string(b)).Template()) 123 } 124 for _, w := range step.Execute.Workflows { 125 b, err := json.Marshal(w) 126 if err != nil { 127 return nil, errors.Wrap(err, "execute: serializing TestWorkflow") 128 } 129 args = append(args, "-w", expressionstcl.NewStringValue(string(b)).Template()) 130 } 131 if step.Execute.Async { 132 args = append(args, "--async") 133 } 134 if step.Execute.Parallelism > 0 { 135 args = append(args, "-p", strconv.Itoa(int(step.Execute.Parallelism))) 136 } 137 container.SetArgs(args...) 138 139 // Add default label 140 types := make([]string, 0) 141 if hasWorkflows { 142 types = append(types, "test workflows") 143 } 144 if hasTests { 145 types = append(types, "tests") 146 } 147 stage.SetCategory("Execute " + strings.Join(types, " & ")) 148 149 return stage, nil 150 } 151 152 func ProcessContentFiles(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 153 if step.Content == nil { 154 return nil, nil 155 } 156 for _, f := range step.Content.Files { 157 if f.ContentFrom == nil { 158 vm, err := layer.AddTextFile(f.Content) 159 if err != nil { 160 return nil, fmt.Errorf("file %s: could not append: %s", f.Path, err.Error()) 161 } 162 vm.MountPath = f.Path 163 container.AppendVolumeMounts(vm) 164 continue 165 } 166 167 volRef := "{{execution.id}}-" + layer.NextRef() 168 169 if f.ContentFrom.ConfigMapKeyRef != nil { 170 layer.AddVolume(corev1.Volume{ 171 Name: volRef, 172 VolumeSource: corev1.VolumeSource{ 173 ConfigMap: &corev1.ConfigMapVolumeSource{ 174 LocalObjectReference: f.ContentFrom.ConfigMapKeyRef.LocalObjectReference, 175 Items: []corev1.KeyToPath{{Key: f.ContentFrom.ConfigMapKeyRef.Key, Path: "file"}}, 176 DefaultMode: f.Mode, 177 Optional: f.ContentFrom.ConfigMapKeyRef.Optional, 178 }, 179 }, 180 }) 181 container.AppendVolumeMounts(corev1.VolumeMount{Name: volRef, MountPath: f.Path, SubPath: "file"}) 182 } else if f.ContentFrom.SecretKeyRef != nil { 183 layer.AddVolume(corev1.Volume{ 184 Name: volRef, 185 VolumeSource: corev1.VolumeSource{ 186 Secret: &corev1.SecretVolumeSource{ 187 SecretName: f.ContentFrom.SecretKeyRef.Name, 188 Items: []corev1.KeyToPath{{Key: f.ContentFrom.SecretKeyRef.Key, Path: "file"}}, 189 DefaultMode: f.Mode, 190 Optional: f.ContentFrom.SecretKeyRef.Optional, 191 }, 192 }, 193 }) 194 container.AppendVolumeMounts(corev1.VolumeMount{Name: volRef, MountPath: f.Path, SubPath: "file"}) 195 } else if f.ContentFrom.FieldRef != nil || f.ContentFrom.ResourceFieldRef != nil { 196 layer.AddVolume(corev1.Volume{ 197 Name: volRef, 198 VolumeSource: corev1.VolumeSource{ 199 Projected: &corev1.ProjectedVolumeSource{ 200 Sources: []corev1.VolumeProjection{{ 201 DownwardAPI: &corev1.DownwardAPIProjection{ 202 Items: []corev1.DownwardAPIVolumeFile{{ 203 Path: "file", 204 FieldRef: f.ContentFrom.FieldRef, 205 ResourceFieldRef: f.ContentFrom.ResourceFieldRef, 206 Mode: f.Mode, 207 }}, 208 }, 209 }}, 210 DefaultMode: f.Mode, 211 }, 212 }, 213 }) 214 container.AppendVolumeMounts(corev1.VolumeMount{Name: volRef, MountPath: f.Path, SubPath: "file"}) 215 } else { 216 return nil, fmt.Errorf("file %s: unrecognized ContentFrom provided for file", f.Path) 217 } 218 } 219 return nil, nil 220 } 221 222 func ProcessContentGit(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 223 if step.Content == nil || step.Content.Git == nil { 224 return nil, nil 225 } 226 227 selfContainer := container.CreateChild() 228 stage := NewContainerStage(layer.NextRef(), selfContainer) 229 stage.SetRetryPolicy(step.Retry) 230 stage.SetCategory("Clone Git repository") 231 232 // Compute mount path 233 mountPath := step.Content.Git.MountPath 234 if mountPath == "" { 235 mountPath = filepath.Join(constants.DefaultDataPath, "repo") 236 } 237 238 // Build volume pair and share with all siblings 239 volumeMount := layer.AddEmptyDirVolume(nil, mountPath) 240 container.AppendVolumeMounts(volumeMount) 241 242 selfContainer. 243 SetWorkingDir("/"). 244 SetImage(constants.DefaultToolkitImage). 245 SetImagePullPolicy(corev1.PullIfNotPresent). 246 SetCommand("/toolkit", "clone", step.Content.Git.Uri). 247 EnableToolkit(stage.Ref()) 248 249 args := []string{mountPath} 250 251 // Provide Git username 252 if step.Content.Git.UsernameFrom != nil { 253 container.AppendEnv(corev1.EnvVar{Name: "TK_GIT_USERNAME", ValueFrom: step.Content.Git.UsernameFrom}) 254 args = append(args, "-u", "{{env.TK_GIT_USERNAME}}") 255 } else if step.Content.Git.Username != "" { 256 args = append(args, "-u", step.Content.Git.Username) 257 } 258 259 // Provide Git token 260 if step.Content.Git.TokenFrom != nil { 261 container.AppendEnv(corev1.EnvVar{Name: "TK_GIT_TOKEN", ValueFrom: step.Content.Git.TokenFrom}) 262 args = append(args, "-t", "{{env.TK_GIT_TOKEN}}") 263 } else if step.Content.Git.Token != "" { 264 args = append(args, "-t", step.Content.Git.Token) 265 } 266 267 // Provide auth type 268 if step.Content.Git.AuthType != "" { 269 args = append(args, "-a", string(step.Content.Git.AuthType)) 270 } 271 272 // Provide revision 273 if step.Content.Git.Revision != "" { 274 args = append(args, "-r", step.Content.Git.Revision) 275 } 276 277 // Provide sparse paths 278 if len(step.Content.Git.Paths) > 0 { 279 for _, pattern := range step.Content.Git.Paths { 280 args = append(args, "-p", pattern) 281 } 282 } 283 284 selfContainer.SetArgs(args...) 285 286 return stage, nil 287 } 288 289 func ProcessArtifacts(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) { 290 if step.Artifacts == nil { 291 return nil, nil 292 } 293 294 if len(step.Artifacts.Paths) == 0 { 295 return nil, errors.New("there needs to be at least one path to scrap for artifacts") 296 } 297 298 selfContainer := container.CreateChild(). 299 ApplyCR(&testworkflowsv1.ContainerConfig{WorkingDir: step.Artifacts.WorkingDir}) 300 stage := NewContainerStage(layer.NextRef(), selfContainer) 301 stage.SetRetryPolicy(step.Retry) 302 stage.SetCondition("always") 303 stage.SetCategory("Upload artifacts") 304 305 selfContainer. 306 SetImage(constants.DefaultToolkitImage). 307 SetImagePullPolicy(corev1.PullIfNotPresent). 308 SetCommand("/toolkit", "artifacts", "-m", constants.DefaultDataPath). 309 EnableToolkit(stage.Ref()) 310 311 args := make([]string, 0) 312 if step.Artifacts.Compress != nil { 313 args = append(args, "--compress", step.Artifacts.Compress.Name) 314 } 315 args = append(args, step.Artifacts.Paths...) 316 selfContainer.SetArgs(args...) 317 318 return stage, nil 319 }