github.com/kubeshop/testkube@v1.17.23/pkg/tcl/testworkflowstcl/testworkflowprocessor/container.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 "maps" 13 "path/filepath" 14 "slices" 15 "strings" 16 17 "github.com/pkg/errors" 18 corev1 "k8s.io/api/core/v1" 19 quantity "k8s.io/apimachinery/pkg/api/resource" 20 21 testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" 22 "github.com/kubeshop/testkube/internal/common" 23 "github.com/kubeshop/testkube/pkg/imageinspector" 24 "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" 25 "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowresolver" 26 ) 27 28 type container struct { 29 parent *container 30 Cr testworkflowsv1.ContainerConfig `expr:"include"` 31 } 32 33 type ContainerComposition interface { 34 Root() Container 35 Parent() Container 36 CreateChild() Container 37 38 Resolve(m ...expressionstcl.Machine) error 39 } 40 41 type ContainerAccessors interface { 42 Env() []corev1.EnvVar 43 EnvFrom() []corev1.EnvFromSource 44 VolumeMounts() []corev1.VolumeMount 45 46 ImagePullPolicy() corev1.PullPolicy 47 Image() string 48 Command() []string 49 Args() []string 50 WorkingDir() string 51 52 Detach() Container 53 ToKubernetesTemplate() (corev1.Container, error) 54 55 Resources() testworkflowsv1.Resources 56 SecurityContext() *corev1.SecurityContext 57 } 58 59 type ContainerMutations[T any] interface { 60 AppendEnv(env ...corev1.EnvVar) T 61 AppendEnvMap(env map[string]string) T 62 AppendEnvFrom(envFrom ...corev1.EnvFromSource) T 63 AppendVolumeMounts(volumeMounts ...corev1.VolumeMount) T 64 SetImagePullPolicy(policy corev1.PullPolicy) T 65 SetImage(image string) T 66 SetCommand(command ...string) T 67 SetArgs(args ...string) T 68 SetWorkingDir(workingDir string) T // "" = default to the image 69 SetResources(resources testworkflowsv1.Resources) T 70 SetSecurityContext(sc *corev1.SecurityContext) T 71 72 ApplyCR(cr *testworkflowsv1.ContainerConfig) T 73 ApplyImageData(image *imageinspector.Info) error 74 EnableToolkit(ref string) T 75 } 76 77 //go:generate mockgen -destination=./mock_container.go -package=testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" Container 78 type Container interface { 79 ContainerComposition 80 ContainerAccessors 81 ContainerMutations[Container] 82 } 83 84 func NewContainer() Container { 85 return &container{} 86 } 87 88 func sum[T any](s1 []T, s2 []T) []T { 89 if len(s1) == 0 { 90 return s2 91 } 92 if len(s2) == 0 { 93 return s1 94 } 95 return append(append(make([]T, 0, len(s1)+len(s2)), s1...), s2...) 96 } 97 98 // Composition 99 100 func (c *container) Root() Container { 101 if c.parent == nil { 102 return c 103 } 104 return c.parent.Parent() 105 } 106 107 func (c *container) Parent() Container { 108 return c.parent 109 } 110 111 func (c *container) CreateChild() Container { 112 return &container{parent: c} 113 } 114 115 // Getters 116 117 func (c *container) Env() []corev1.EnvVar { 118 if c.parent == nil { 119 return c.Cr.Env 120 } 121 return sum(c.parent.Env(), c.Cr.Env) 122 } 123 124 func (c *container) EnvFrom() []corev1.EnvFromSource { 125 if c.parent == nil { 126 return c.Cr.EnvFrom 127 } 128 return sum(c.parent.EnvFrom(), c.Cr.EnvFrom) 129 } 130 131 func (c *container) VolumeMounts() []corev1.VolumeMount { 132 if c.parent == nil { 133 return c.Cr.VolumeMounts 134 } 135 return sum(c.parent.VolumeMounts(), c.Cr.VolumeMounts) 136 } 137 138 func (c *container) ImagePullPolicy() corev1.PullPolicy { 139 if c.parent == nil || c.Cr.ImagePullPolicy != "" { 140 return c.Cr.ImagePullPolicy 141 } 142 return c.parent.ImagePullPolicy() 143 } 144 145 func (c *container) Image() string { 146 if c.parent == nil || c.Cr.Image != "" { 147 return c.Cr.Image 148 } 149 return c.parent.Image() 150 } 151 152 func (c *container) Command() []string { 153 // Do not inherit command, if the Image was replaced on this depth 154 if c.parent == nil || c.Cr.Command != nil || (c.Cr.Image != "" && c.Cr.Image != c.Image()) { 155 if c.Cr.Command == nil { 156 return nil 157 } 158 return *c.Cr.Command 159 } 160 return c.parent.Command() 161 } 162 163 func (c *container) Args() []string { 164 // Do not inherit args, if the Image or Command was replaced on this depth 165 if c.parent == nil || (c.Cr.Args != nil && len(*c.Cr.Args) > 0) || c.Cr.Command != nil || (c.Cr.Image != "" && c.Cr.Image != c.Image()) { 166 if c.Cr.Args == nil { 167 return nil 168 } 169 return *c.Cr.Args 170 } 171 return c.parent.Args() 172 } 173 174 func (c *container) WorkingDir() string { 175 path := "" 176 if c.Cr.WorkingDir != nil { 177 path = *c.Cr.WorkingDir 178 } 179 if c.parent == nil { 180 return path 181 } 182 if filepath.IsAbs(path) { 183 return path 184 } 185 parentPath := c.parent.WorkingDir() 186 if parentPath == "" { 187 return path 188 } 189 return filepath.Join(parentPath, path) 190 } 191 192 func (c *container) Resources() (r testworkflowsv1.Resources) { 193 if c.parent != nil { 194 r = *common.Ptr(c.parent.Resources()).DeepCopy() 195 } 196 if c.Cr.Resources == nil { 197 return 198 } 199 if len(c.Cr.Resources.Requests) > 0 { 200 r.Requests = c.Cr.Resources.Requests 201 } 202 if len(c.Cr.Resources.Limits) > 0 { 203 r.Limits = c.Cr.Resources.Limits 204 } 205 return 206 } 207 208 func (c *container) SecurityContext() *corev1.SecurityContext { 209 if c.Cr.SecurityContext != nil { 210 return c.Cr.SecurityContext 211 } 212 if c.parent == nil { 213 return nil 214 } 215 return c.parent.SecurityContext() 216 } 217 218 // Mutations 219 220 func (c *container) AppendEnv(env ...corev1.EnvVar) Container { 221 c.Cr.Env = append(c.Cr.Env, env...) 222 return c 223 } 224 225 func (c *container) AppendEnvMap(env map[string]string) Container { 226 for k, v := range env { 227 c.Cr.Env = append(c.Cr.Env, corev1.EnvVar{Name: k, Value: v}) 228 } 229 return c 230 } 231 232 func (c *container) AppendEnvFrom(envFrom ...corev1.EnvFromSource) Container { 233 c.Cr.EnvFrom = append(c.Cr.EnvFrom, envFrom...) 234 return c 235 } 236 237 func (c *container) AppendVolumeMounts(volumeMounts ...corev1.VolumeMount) Container { 238 c.Cr.VolumeMounts = append(c.Cr.VolumeMounts, volumeMounts...) 239 return c 240 } 241 242 func (c *container) SetImagePullPolicy(policy corev1.PullPolicy) Container { 243 c.Cr.ImagePullPolicy = policy 244 return c 245 } 246 247 func (c *container) SetImage(image string) Container { 248 c.Cr.Image = image 249 return c 250 } 251 252 func (c *container) SetCommand(command ...string) Container { 253 c.Cr.Command = &command 254 return c 255 } 256 257 func (c *container) SetArgs(args ...string) Container { 258 c.Cr.Args = &args 259 return c 260 } 261 262 func (c *container) SetWorkingDir(workingDir string) Container { 263 c.Cr.WorkingDir = &workingDir 264 return c 265 } 266 267 func (c *container) SetResources(resources testworkflowsv1.Resources) Container { 268 c.Cr.Resources = &resources 269 return c 270 } 271 272 func (c *container) SetSecurityContext(sc *corev1.SecurityContext) Container { 273 c.Cr.SecurityContext = sc 274 return c 275 } 276 277 func (c *container) ApplyCR(config *testworkflowsv1.ContainerConfig) Container { 278 c.Cr = *testworkflowresolver.MergeContainerConfig(&c.Cr, config) 279 return c 280 } 281 282 func (c *container) ToContainerConfig() testworkflowsv1.ContainerConfig { 283 env := slices.Clone(c.Env()) 284 for i := range env { 285 env[i] = *env[i].DeepCopy() 286 } 287 envFrom := slices.Clone(c.EnvFrom()) 288 for i := range envFrom { 289 envFrom[i] = *envFrom[i].DeepCopy() 290 } 291 volumeMounts := slices.Clone(c.VolumeMounts()) 292 for i := range volumeMounts { 293 volumeMounts[i] = *volumeMounts[i].DeepCopy() 294 } 295 return testworkflowsv1.ContainerConfig{ 296 WorkingDir: common.Ptr(c.WorkingDir()), 297 Image: c.Image(), 298 ImagePullPolicy: c.ImagePullPolicy(), 299 Env: env, 300 EnvFrom: envFrom, 301 Command: common.Ptr(slices.Clone(c.Command())), 302 Args: common.Ptr(slices.Clone(c.Args())), 303 Resources: &testworkflowsv1.Resources{ 304 Requests: maps.Clone(c.Resources().Requests), 305 Limits: maps.Clone(c.Resources().Limits), 306 }, 307 SecurityContext: c.SecurityContext().DeepCopy(), 308 VolumeMounts: volumeMounts, 309 } 310 } 311 312 func (c *container) Detach() Container { 313 c.Cr = c.ToContainerConfig() 314 c.parent = nil 315 return c 316 } 317 318 func (c *container) ToKubernetesTemplate() (corev1.Container, error) { 319 cr := c.ToContainerConfig() 320 var command []string 321 if cr.Command != nil { 322 command = *cr.Command 323 } 324 var args []string 325 if cr.Args != nil { 326 args = *cr.Args 327 } 328 workingDir := "" 329 if cr.WorkingDir != nil { 330 workingDir = *cr.WorkingDir 331 } 332 resources := corev1.ResourceRequirements{} 333 if cr.Resources != nil { 334 if len(cr.Resources.Requests) > 0 { 335 resources.Requests = make(corev1.ResourceList) 336 } 337 if len(cr.Resources.Limits) > 0 { 338 resources.Limits = make(corev1.ResourceList) 339 } 340 for k, v := range cr.Resources.Requests { 341 var err error 342 resources.Requests[k], err = quantity.ParseQuantity(v.String()) 343 if err != nil { 344 return corev1.Container{}, errors.Wrap(err, "parsing resources") 345 } 346 } 347 for k, v := range cr.Resources.Limits { 348 var err error 349 resources.Limits[k], err = quantity.ParseQuantity(v.String()) 350 if err != nil { 351 return corev1.Container{}, errors.Wrap(err, "parsing resources") 352 } 353 } 354 } 355 return corev1.Container{ 356 Image: cr.Image, 357 ImagePullPolicy: cr.ImagePullPolicy, 358 Command: command, 359 Args: args, 360 Env: cr.Env, 361 EnvFrom: cr.EnvFrom, 362 VolumeMounts: cr.VolumeMounts, 363 Resources: resources, 364 WorkingDir: workingDir, 365 SecurityContext: cr.SecurityContext, 366 }, nil 367 } 368 369 func (c *container) ApplyImageData(image *imageinspector.Info) error { 370 if image == nil { 371 return nil 372 } 373 err := c.Resolve(expressionstcl.NewMachine(). 374 Register("image.command", image.Entrypoint). 375 Register("image.args", image.Cmd). 376 Register("image.workingDir", image.WorkingDir)) 377 if err != nil { 378 return err 379 } 380 if len(c.Command()) == 0 { 381 args := c.Args() 382 c.SetCommand(image.Entrypoint...) 383 if len(args) == 0 { 384 c.SetArgs(image.Cmd...) 385 } else { 386 c.SetArgs(args...) 387 } 388 } 389 if image.WorkingDir != "" && c.WorkingDir() == "" { 390 c.SetWorkingDir(image.WorkingDir) 391 } 392 return nil 393 } 394 395 func (c *container) EnableToolkit(ref string) Container { 396 return c. 397 AppendEnv(corev1.EnvVar{ 398 Name: "TK_IP", 399 ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "status.podIP"}}, 400 }). 401 AppendEnvMap(map[string]string{ 402 "TK_REF": ref, 403 "TK_NS": "{{internal.namespace}}", 404 "TK_TMPL": "{{internal.globalTemplate}}", 405 "TK_WF": "{{workflow.name}}", 406 "TK_EX": "{{execution.id}}", 407 "TK_C_URL": "{{internal.cloud.api.url}}", 408 "TK_C_KEY": "{{internal.cloud.api.key}}", 409 "TK_C_TLS_INSECURE": "{{internal.cloud.api.tlsInsecure}}", 410 "TK_C_SKIP_VERIFY": "{{internal.cloud.api.skipVerify}}", 411 "TK_OS_ENDPOINT": "{{internal.storage.url}}", 412 "TK_OS_ACCESSKEY": "{{internal.storage.accessKey}}", 413 "TK_OS_SECRETKEY": "{{internal.storage.secretKey}}", 414 "TK_OS_REGION": "{{internal.storage.region}}", 415 "TK_OS_TOKEN": "{{internal.storage.token}}", 416 "TK_OS_BUCKET": "{{internal.storage.bucket}}", 417 "TK_OS_SSL": "{{internal.storage.ssl}}", 418 "TK_OS_SSL_SKIP_VERIFY": "{{internal.storage.skipVerify}}", 419 "TK_OS_CERT_FILE": "{{internal.storage.certFile}}", 420 "TK_OS_KEY_FILE": "{{internal.storage.keyFile}}", 421 "TK_OS_CA_FILE": "{{internal.storage.caFile}}", 422 "TK_IMG_TOOLKIT": "{{internal.images.toolkit}}", 423 "TK_IMG_INIT": "{{internal.images.init}}", 424 }) 425 } 426 427 func (c *container) Resolve(m ...expressionstcl.Machine) error { 428 base := expressionstcl.NewMachine(). 429 RegisterAccessor(func(name string) (interface{}, bool) { 430 if !strings.HasPrefix(name, "env.") { 431 return nil, false 432 } 433 env := c.Env() 434 name = name[4:] 435 for i := range env { 436 if env[i].Name == name { 437 value, err := expressionstcl.EvalTemplate(env[i].Value) 438 if err == nil { 439 return value, true 440 } 441 break 442 } 443 } 444 return nil, false 445 }) 446 return expressionstcl.Simplify(c, append([]expressionstcl.Machine{base}, m...)...) 447 }