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  }