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 }