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  }