github.com/kubeshop/testkube@v1.17.23/pkg/executor/containerexecutor/tmpl.go (about)

     1  package containerexecutor
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	_ "embed"
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"go.uber.org/zap"
    14  	batchv1 "k8s.io/api/batch/v1"
    15  	corev1 "k8s.io/api/core/v1"
    16  	"k8s.io/apimachinery/pkg/util/yaml"
    17  	kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
    18  	"sigs.k8s.io/kustomize/kyaml/yaml/merge2"
    19  
    20  	templatesv1 "github.com/kubeshop/testkube-operator/pkg/client/templates/v1"
    21  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    22  	"github.com/kubeshop/testkube/pkg/executor"
    23  	"github.com/kubeshop/testkube/pkg/executor/client"
    24  	"github.com/kubeshop/testkube/pkg/executor/env"
    25  	"github.com/kubeshop/testkube/pkg/imageinspector"
    26  	"github.com/kubeshop/testkube/pkg/utils"
    27  )
    28  
    29  const (
    30  	// EntrypointScriptName is entrypoint script name
    31  	EntrypointScriptName = "entrypoint.sh"
    32  )
    33  
    34  //go:embed templates/job.tmpl
    35  var defaultJobTemplate string
    36  
    37  // NewExecutorJobSpec is a method to create new executor job spec
    38  func NewExecutorJobSpec(log *zap.SugaredLogger, options *JobOptions) (*batchv1.Job, error) {
    39  	envManager := env.NewManager()
    40  	secretEnvVars := append(envManager.PrepareSecrets(options.SecretEnvs, options.Variables),
    41  		envManager.PrepareGitCredentials(options.UsernameSecret, options.TokenSecret)...)
    42  
    43  	tmpl, err := utils.NewTemplate("job").Parse(options.JobTemplate)
    44  	if err != nil {
    45  		return nil, fmt.Errorf("creating job spec from executor template error: %w", err)
    46  	}
    47  
    48  	options.Jsn = strings.ReplaceAll(options.Jsn, "'", "''")
    49  	for i := range options.Command {
    50  		if options.Command[i] != "" {
    51  			options.Command[i] = fmt.Sprintf("%q", options.Command[i])
    52  		}
    53  	}
    54  
    55  	for i := range options.Args {
    56  		if options.Args[i] != "" {
    57  			options.Args[i] = fmt.Sprintf("%q", options.Args[i])
    58  		}
    59  	}
    60  
    61  	var buffer bytes.Buffer
    62  	if err = tmpl.ExecuteTemplate(&buffer, "job", options); err != nil {
    63  		return nil, fmt.Errorf("executing job spec executor template: %w", err)
    64  	}
    65  
    66  	var job batchv1.Job
    67  	jobSpec := buffer.String()
    68  	if options.JobTemplateExtensions != "" {
    69  		tmplExt, err := utils.NewTemplate("jobExt").Parse(options.JobTemplateExtensions)
    70  		if err != nil {
    71  			return nil, fmt.Errorf("creating job extensions spec from executor template error: %w", err)
    72  		}
    73  
    74  		var bufferExt bytes.Buffer
    75  		if err = tmplExt.ExecuteTemplate(&bufferExt, "jobExt", options); err != nil {
    76  			return nil, fmt.Errorf("executing job extensions spec executor template: %w", err)
    77  		}
    78  
    79  		if jobSpec, err = merge2.MergeStrings(bufferExt.String(), jobSpec, false, kyaml.MergeOptions{}); err != nil {
    80  			return nil, fmt.Errorf("merging job spec executor templates: %w", err)
    81  		}
    82  	}
    83  
    84  	decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(jobSpec), len(jobSpec))
    85  	if err := decoder.Decode(&job); err != nil {
    86  		return nil, fmt.Errorf("decoding executor job spec error: %w", err)
    87  	}
    88  
    89  	for key, value := range options.Labels {
    90  		if job.Labels == nil {
    91  			job.Labels = make(map[string]string)
    92  		}
    93  
    94  		job.Labels[key] = value
    95  
    96  		if job.Spec.Template.Labels == nil {
    97  			job.Spec.Template.Labels = make(map[string]string)
    98  		}
    99  
   100  		job.Spec.Template.Labels[key] = value
   101  	}
   102  
   103  	envs := append(executor.RunnerEnvVars, corev1.EnvVar{Name: "RUNNER_CLUSTERID", Value: options.ClusterID})
   104  	if options.ArtifactRequest != nil && options.ArtifactRequest.StorageBucket != "" {
   105  		envs = append(envs, corev1.EnvVar{Name: "RUNNER_BUCKET", Value: options.ArtifactRequest.StorageBucket})
   106  	} else {
   107  		envs = append(envs, corev1.EnvVar{Name: "RUNNER_BUCKET", Value: os.Getenv("STORAGE_BUCKET")})
   108  	}
   109  
   110  	envs = append(envs, secretEnvVars...)
   111  	if options.HTTPProxy != "" {
   112  		envs = append(envs, corev1.EnvVar{Name: "HTTP_PROXY", Value: options.HTTPProxy})
   113  	}
   114  
   115  	if options.HTTPSProxy != "" {
   116  		envs = append(envs, corev1.EnvVar{Name: "HTTPS_PROXY", Value: options.HTTPSProxy})
   117  	}
   118  
   119  	envs = append(envs, envManager.PrepareEnvs(options.Envs, options.Variables)...)
   120  	envs = append(envs, corev1.EnvVar{Name: "RUNNER_WORKINGDIR", Value: options.WorkingDir})
   121  	envs = append(envs, corev1.EnvVar{Name: "RUNNER_EXECUTIONID", Value: options.Name})
   122  	envs = append(envs, corev1.EnvVar{Name: "RUNNER_TESTNAME", Value: options.TestName})
   123  	envs = append(envs, corev1.EnvVar{Name: "RUNNER_EXECUTIONNUMBER", Value: fmt.Sprint(options.ExecutionNumber)})
   124  	envs = append(envs, corev1.EnvVar{Name: "RUNNER_CONTEXTTYPE", Value: options.ContextType})
   125  	envs = append(envs, corev1.EnvVar{Name: "RUNNER_CONTEXTDATA", Value: options.ContextData})
   126  	envs = append(envs, corev1.EnvVar{Name: "RUNNER_APIURI", Value: options.APIURI})
   127  
   128  	// envs needed for logs sidecar
   129  	if options.Features.LogsV2 {
   130  		envs = append(envs, corev1.EnvVar{Name: "ID", Value: options.Name})
   131  		envs = append(envs, corev1.EnvVar{Name: "NATS_URI", Value: options.NatsUri})
   132  		envs = append(envs, corev1.EnvVar{Name: "NAMESPACE", Value: options.Namespace})
   133  	}
   134  
   135  	for i := range job.Spec.Template.Spec.InitContainers {
   136  		job.Spec.Template.Spec.InitContainers[i].Env = append(job.Spec.Template.Spec.InitContainers[i].Env, envs...)
   137  	}
   138  
   139  	for i := range job.Spec.Template.Spec.Containers {
   140  		job.Spec.Template.Spec.Containers[i].Env = append(job.Spec.Template.Spec.Containers[i].Env, envs...)
   141  	}
   142  
   143  	return &job, nil
   144  }
   145  
   146  // NewScraperJobSpec is a method to create new scraper job spec
   147  func NewScraperJobSpec(log *zap.SugaredLogger, options *JobOptions) (*batchv1.Job, error) {
   148  	tmpl, err := utils.NewTemplate("job").Parse(options.ScraperTemplate)
   149  	if err != nil {
   150  		return nil, fmt.Errorf("creating job spec from scraper template error: %w", err)
   151  	}
   152  
   153  	options.Jsn = strings.ReplaceAll(options.Jsn, "'", "''")
   154  	var buffer bytes.Buffer
   155  	if err = tmpl.ExecuteTemplate(&buffer, "job", options); err != nil {
   156  		return nil, fmt.Errorf("executing job spec scraper template: %w", err)
   157  	}
   158  
   159  	var job batchv1.Job
   160  	jobSpec := buffer.String()
   161  	if options.ScraperTemplateExtensions != "" {
   162  		tmplExt, err := utils.NewTemplate("jobExt").Parse(options.ScraperTemplateExtensions)
   163  		if err != nil {
   164  			return nil, fmt.Errorf("creating scraper extensions spec from executor template error: %w", err)
   165  		}
   166  
   167  		var bufferExt bytes.Buffer
   168  		if err = tmplExt.ExecuteTemplate(&bufferExt, "jobExt", options); err != nil {
   169  			return nil, fmt.Errorf("executing scraper extensions spec executor template: %w", err)
   170  		}
   171  
   172  		if jobSpec, err = merge2.MergeStrings(bufferExt.String(), jobSpec, false, kyaml.MergeOptions{}); err != nil {
   173  			return nil, fmt.Errorf("merging scraper spec executor templates: %w", err)
   174  		}
   175  	}
   176  
   177  	log.Debug("Scraper job specification", jobSpec)
   178  	decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(jobSpec), len(jobSpec))
   179  	if err := decoder.Decode(&job); err != nil {
   180  		return nil, fmt.Errorf("decoding scraper job spec error: %w", err)
   181  	}
   182  
   183  	envs := append(executor.RunnerEnvVars, corev1.EnvVar{Name: "RUNNER_CLUSTERID", Value: options.ClusterID})
   184  	if options.ArtifactRequest != nil && options.ArtifactRequest.StorageBucket != "" {
   185  		envs = append(envs, corev1.EnvVar{Name: "RUNNER_BUCKET", Value: options.ArtifactRequest.StorageBucket})
   186  	} else {
   187  		envs = append(envs, corev1.EnvVar{Name: "RUNNER_BUCKET", Value: os.Getenv("STORAGE_BUCKET")})
   188  	}
   189  
   190  	if options.HTTPProxy != "" {
   191  		envs = append(envs, corev1.EnvVar{Name: "HTTP_PROXY", Value: options.HTTPProxy})
   192  	}
   193  
   194  	if options.HTTPSProxy != "" {
   195  		envs = append(envs, corev1.EnvVar{Name: "HTTPS_PROXY", Value: options.HTTPSProxy})
   196  	}
   197  
   198  	for i := range job.Spec.Template.Spec.Containers {
   199  		job.Spec.Template.Spec.Containers[i].Env = append(job.Spec.Template.Spec.Containers[i].Env, envs...)
   200  	}
   201  
   202  	return &job, nil
   203  }
   204  
   205  // TODO refactor JobOptions to use builder pattern
   206  // TODO extract JobOptions for both container and job executor to common package in separate PR
   207  // NewJobOptions provides job options for templates
   208  func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface, images executor.Images,
   209  	templates executor.Templates, inspector imageinspector.Inspector, serviceAccountNames map[string]string,
   210  	registry, clusterID, apiURI string, execution testkube.Execution, options client.ExecuteOptions, natsUri string, debug bool) (*JobOptions, error) {
   211  	jobOptions := NewJobOptionsFromExecutionOptions(options)
   212  	if execution.PreRunScript != "" || execution.PostRunScript != "" {
   213  		jobOptions.Command = []string{filepath.Join(executor.VolumeDir, EntrypointScriptName)}
   214  		if jobOptions.Image != "" {
   215  			info, err := inspector.Inspect(context.Background(), registry, jobOptions.Image, corev1.PullIfNotPresent, jobOptions.ImagePullSecrets)
   216  			if err == nil {
   217  				if len(execution.Command) == 0 {
   218  					execution.Command = info.Cmd
   219  				}
   220  				execution.ContainerShell = info.Shell
   221  			} else {
   222  				log.Errorw("Docker image inspection error", "error", err)
   223  			}
   224  		}
   225  	}
   226  
   227  	jsn, err := json.Marshal(execution)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	jobOptions.Name = execution.Id
   233  	jobOptions.Namespace = execution.TestNamespace
   234  	jobOptions.TestName = execution.TestName
   235  	jobOptions.Jsn = string(jsn)
   236  	jobOptions.InitImage = images.Init
   237  	jobOptions.ScraperImage = images.Scraper
   238  
   239  	// options needed for Log sidecar
   240  	if options.Features.LogsV2 {
   241  		// TODO pass them from some config? we dont' have any in this context?
   242  		jobOptions.Debug = debug
   243  		jobOptions.NatsUri = natsUri
   244  		jobOptions.LogSidecarImage = images.LogSidecar
   245  	}
   246  
   247  	if jobOptions.JobTemplate == "" {
   248  		jobOptions.JobTemplate = templates.Job
   249  		if jobOptions.JobTemplate == "" {
   250  			jobOptions.JobTemplate = defaultJobTemplate
   251  		}
   252  	}
   253  
   254  	if options.ExecutorSpec.JobTemplateReference != "" {
   255  		template, err := templatesClient.Get(options.ExecutorSpec.JobTemplateReference)
   256  		if err != nil {
   257  			return jobOptions, err
   258  		}
   259  
   260  		if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.CONTAINER_TemplateType {
   261  			jobOptions.JobTemplate = template.Spec.Body
   262  		} else {
   263  			log.Warnw("Not matched template type", "template", options.ExecutorSpec.JobTemplateReference)
   264  		}
   265  	}
   266  
   267  	if options.Request.JobTemplateReference != "" {
   268  		template, err := templatesClient.Get(options.Request.JobTemplateReference)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  
   273  		if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.CONTAINER_TemplateType {
   274  			jobOptions.JobTemplate = template.Spec.Body
   275  		} else {
   276  			log.Warnw("Not matched template type", "template", options.Request.JobTemplateReference)
   277  		}
   278  	}
   279  
   280  	jobOptions.ScraperTemplate = templates.Scraper
   281  	if options.Request.ScraperTemplateReference != "" {
   282  		template, err := templatesClient.Get(options.Request.ScraperTemplateReference)
   283  		if err != nil {
   284  			return nil, err
   285  		}
   286  
   287  		if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.SCRAPER_TemplateType {
   288  			jobOptions.ScraperTemplate = template.Spec.Body
   289  		} else {
   290  			log.Warnw("Not matched template type", "template", options.Request.ScraperTemplateReference)
   291  		}
   292  	}
   293  
   294  	jobOptions.PvcTemplate = templates.PVC
   295  	if options.Request.PvcTemplateReference != "" {
   296  		template, err := templatesClient.Get(options.Request.PvcTemplateReference)
   297  		if err != nil {
   298  			return nil, err
   299  		}
   300  
   301  		if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.PVC_TemplateType {
   302  			jobOptions.PvcTemplate = template.Spec.Body
   303  		} else {
   304  			log.Warnw("Not matched template type", "template", options.Request.PvcTemplateReference)
   305  		}
   306  	}
   307  
   308  	jobOptions.Variables = execution.Variables
   309  	serviceAccountName, ok := serviceAccountNames[execution.TestNamespace]
   310  	if !ok {
   311  		return jobOptions, fmt.Errorf("not supported namespace %s", execution.TestNamespace)
   312  	}
   313  
   314  	jobOptions.ServiceAccountName = serviceAccountName
   315  	jobOptions.Registry = registry
   316  	jobOptions.ClusterID = clusterID
   317  	jobOptions.APIURI = apiURI
   318  
   319  	return jobOptions, nil
   320  }