sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/pod-utils/decorate/podspec.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package decorate 18 19 import ( 20 "fmt" 21 "path" 22 "path/filepath" 23 "sort" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/sirupsen/logrus" 29 coreapi "k8s.io/api/core/v1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/util/sets" 32 "k8s.io/apimachinery/pkg/util/validation" 33 34 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 35 "sigs.k8s.io/prow/pkg/clonerefs" 36 "sigs.k8s.io/prow/pkg/entrypoint" 37 "sigs.k8s.io/prow/pkg/gcsupload" 38 "sigs.k8s.io/prow/pkg/github" 39 "sigs.k8s.io/prow/pkg/initupload" 40 "sigs.k8s.io/prow/pkg/kube" 41 "sigs.k8s.io/prow/pkg/pod-utils/clone" 42 "sigs.k8s.io/prow/pkg/pod-utils/downwardapi" 43 "sigs.k8s.io/prow/pkg/pod-utils/wrapper" 44 "sigs.k8s.io/prow/pkg/sidecar" 45 ) 46 47 const ( 48 logMountName = "logs" 49 logMountPath = "/logs" 50 artifactsEnv = "ARTIFACTS" 51 artifactsPath = logMountPath + "/artifacts" 52 codeMountName = "code" 53 codeMountPath = "/home/prow/go" 54 gopathEnv = "GOPATH" 55 toolsMountName = "tools" 56 toolsMountPath = "/tools" 57 gcsCredentialsMountName = "gcs-credentials" 58 gcsCredentialsMountPath = "/secrets/gcs" 59 s3CredentialsMountName = "s3-credentials" 60 s3CredentialsMountPath = "/secrets/s3-storage" 61 outputMountName = "output" 62 outputMountPath = "/output" 63 ) 64 65 // Labels returns a string slice with label consts from kube. 66 func Labels() []string { 67 return []string{kube.ProwJobTypeLabel, kube.CreatedByProw, kube.ProwJobIDLabel} 68 } 69 70 // VolumeMounts returns a string set with *MountName consts in it. 71 func VolumeMounts(dc *prowapi.DecorationConfig) sets.Set[string] { 72 ret := sets.New[string](logMountName, codeMountName, toolsMountName, gcsCredentialsMountName, s3CredentialsMountName) 73 if dc == nil { 74 return ret 75 } 76 77 if dc.OauthTokenSecret != nil { 78 ret.Insert(dc.OauthTokenSecret.Name) 79 } 80 for _, sshKeySecret := range dc.SSHKeySecrets { 81 ret.Insert(sshKeySecret) 82 } 83 return ret 84 } 85 86 // VolumeMountsOnTestContainer returns a string set with *MountName consts in it which are applied to the test container. 87 func VolumeMountsOnTestContainer() sets.Set[string] { 88 return sets.New[string](logMountName, codeMountName, toolsMountName) 89 } 90 91 // VolumeMountPathsOnTestContainer returns a string set with *MountPath consts in it which are applied to the test container. 92 func VolumeMountPathsOnTestContainer() sets.Set[string] { 93 return sets.New[string](logMountPath, codeMountPath, toolsMountPath) 94 } 95 96 // PodUtilsContainerNames returns a string set with pod utility container name consts in it. 97 func PodUtilsContainerNames() sets.Set[string] { 98 return sets.New[string](cloneRefsName, initUploadName, entrypointName, sidecarName) 99 } 100 101 // LabelsAndAnnotationsForSpec returns a minimal set of labels to add to prowjobs or its owned resources. 102 // 103 // User-provided extraLabels and extraAnnotations values will take precedence over auto-provided values. 104 func LabelsAndAnnotationsForSpec(spec prowapi.ProwJobSpec, extraLabels, extraAnnotations map[string]string) (map[string]string, map[string]string) { 105 log := logrus.WithFields(logrus.Fields{ 106 "job": spec.Job, 107 "id": extraLabels[kube.ProwBuildIDLabel], 108 }) 109 labels := map[string]string{ 110 kube.CreatedByProw: "true", 111 kube.ProwJobTypeLabel: string(spec.Type), 112 } 113 annotations := map[string]string{} 114 for key, value := range map[string]string{ 115 kube.ProwJobAnnotation: spec.Job, 116 kube.ContextAnnotation: spec.Context, 117 } { 118 maybeTruncated := value 119 if len(value) > validation.LabelValueMaxLength { 120 // TODO(fejta): consider truncating middle rather than end. 121 maybeTruncated = strings.TrimRight(value[:validation.LabelValueMaxLength], "._-") 122 log.WithFields(logrus.Fields{ 123 "key": key, 124 "value": value, 125 "maybeTruncated": maybeTruncated, 126 }).Info("Cannot use full value, will truncate.") 127 } 128 labels[key] = maybeTruncated 129 annotations[key] = value 130 } 131 132 var refs *prowapi.Refs 133 if spec.Refs != nil { 134 refs = spec.Refs 135 } else if len(spec.ExtraRefs) > 0 { 136 refs = &spec.ExtraRefs[0] 137 } 138 if refs != nil { 139 labels[kube.OrgLabel] = refs.Org 140 labels[kube.RepoLabel] = refs.Repo 141 labels[kube.BaseRefLabel] = refs.BaseRef 142 if len(refs.Pulls) > 0 { 143 labels[kube.PullLabel] = strconv.Itoa(refs.Pulls[0].Number) 144 } 145 } 146 147 for k, v := range extraLabels { 148 labels[k] = v 149 } 150 151 // let's validate labels 152 for key, value := range labels { 153 if errs := validation.IsValidLabelValue(value); len(errs) > 0 { 154 // try to use basename of a path, if path contains invalid // 155 base := filepath.Base(value) 156 if errs := validation.IsValidLabelValue(base); len(errs) == 0 { 157 labels[key] = base 158 continue 159 } 160 log.WithFields(logrus.Fields{ 161 "key": key, 162 "value": value, 163 "errors": errs, 164 }).Warn("Removing invalid label") 165 delete(labels, key) 166 } 167 } 168 169 for k, v := range extraAnnotations { 170 annotations[k] = v 171 } 172 173 return labels, annotations 174 } 175 176 // LabelsAndAnnotationsForJob returns a standard set of labels to add to pod/build/etc resources. 177 func LabelsAndAnnotationsForJob(pj prowapi.ProwJob) (map[string]string, map[string]string) { 178 var extraLabels map[string]string 179 if extraLabels = pj.ObjectMeta.Labels; extraLabels == nil { 180 extraLabels = map[string]string{} 181 } 182 var extraAnnotations map[string]string 183 if extraAnnotations = pj.ObjectMeta.Annotations; extraAnnotations == nil { 184 extraAnnotations = map[string]string{} 185 } 186 extraLabels[kube.ProwJobIDLabel] = pj.ObjectMeta.Name 187 extraLabels[kube.ProwBuildIDLabel] = pj.Status.BuildID 188 return LabelsAndAnnotationsForSpec(pj.Spec, extraLabels, extraAnnotations) 189 } 190 191 // ProwJobToPod converts a ProwJob to a Pod that will run the tests. 192 func ProwJobToPod(pj prowapi.ProwJob) (*coreapi.Pod, error) { 193 return ProwJobToPodLocal(pj, "") 194 } 195 196 // ProwJobToPodLocal converts a ProwJob to a Pod that will run the tests. 197 // If an output directory is specified, files are copied to the dir instead of uploading to GCS if 198 // decoration is configured. 199 func ProwJobToPodLocal(pj prowapi.ProwJob, outputDir string) (*coreapi.Pod, error) { 200 if pj.Spec.PodSpec == nil { 201 return nil, fmt.Errorf("prowjob %q lacks a pod spec", pj.Name) 202 } 203 204 rawEnv, err := downwardapi.EnvForSpec(downwardapi.NewJobSpec(pj.Spec, pj.Status.BuildID, pj.Name)) 205 if err != nil { 206 return nil, err 207 } 208 209 spec := pj.Spec.PodSpec.DeepCopy() 210 spec.RestartPolicy = "Never" 211 if len(spec.Containers) == 1 { 212 spec.Containers[0].Name = kube.TestContainerName 213 } 214 215 // if the user has not provided a serviceaccount to use or explicitly 216 // requested mounting the default token, we treat the unset value as 217 // false, while kubernetes treats it as true if it is unset because 218 // it was added in v1.6 219 if spec.AutomountServiceAccountToken == nil && spec.ServiceAccountName == "" { 220 myFalse := false 221 spec.AutomountServiceAccountToken = &myFalse 222 } 223 224 if pj.Spec.DecorationConfig == nil { 225 for i, container := range spec.Containers { 226 spec.Containers[i].Env = append(container.Env, KubeEnv(rawEnv)...) 227 } 228 } else { 229 if err := decorate(spec, &pj, rawEnv, outputDir); err != nil { 230 return nil, fmt.Errorf("error decorating podspec: %w", err) 231 } 232 } 233 234 // If no termination policy is specified, use log fallback so the pod status 235 // contains a snippet of the failure, which is helpful when pods are cleaned up 236 // or evicted in failure modes. Callers can override by setting explicit policy. 237 for i, container := range spec.InitContainers { 238 if len(container.TerminationMessagePolicy) == 0 { 239 spec.InitContainers[i].TerminationMessagePolicy = coreapi.TerminationMessageFallbackToLogsOnError 240 } 241 } 242 for i, container := range spec.Containers { 243 if len(container.TerminationMessagePolicy) == 0 { 244 spec.Containers[i].TerminationMessagePolicy = coreapi.TerminationMessageFallbackToLogsOnError 245 } 246 } 247 248 podLabels, annotations := LabelsAndAnnotationsForJob(pj) 249 return &coreapi.Pod{ 250 ObjectMeta: metav1.ObjectMeta{ 251 Name: pj.ObjectMeta.Name, 252 Labels: podLabels, 253 Annotations: annotations, 254 }, 255 Spec: *spec, 256 }, nil 257 } 258 259 const cloneLogPath = "clone.json" 260 261 // CloneLogPath returns the path to the clone log file in the volume mount. 262 // CloneLogPath returns the path to the clone log file in the volume mount. 263 func CloneLogPath(logMount coreapi.VolumeMount) string { 264 return filepath.Join(logMount.MountPath, cloneLogPath) 265 } 266 267 // Exposed for testing 268 const ( 269 entrypointName = "place-entrypoint" 270 initUploadName = "initupload" 271 sidecarName = "sidecar" 272 cloneRefsName = "clonerefs" 273 ) 274 275 // cloneEnv encodes clonerefs Options into json and puts it into an environment variable 276 func cloneEnv(opt clonerefs.Options) ([]coreapi.EnvVar, error) { 277 // TODO(fejta): use flags 278 cloneConfigEnv, err := clonerefs.Encode(opt) 279 if err != nil { 280 return nil, err 281 } 282 return KubeEnv(map[string]string{clonerefs.JSONConfigEnvVar: cloneConfigEnv}), nil 283 } 284 285 // tmpVolume creates an emptyDir volume and mount for a tmp folder 286 // This is e.g. used by CloneRefs to store the known hosts file 287 func tmpVolume(name string) (coreapi.Volume, coreapi.VolumeMount) { 288 v := coreapi.Volume{ 289 Name: name, 290 VolumeSource: coreapi.VolumeSource{ 291 EmptyDir: &coreapi.EmptyDirVolumeSource{}, 292 }, 293 } 294 295 vm := coreapi.VolumeMount{ 296 Name: name, 297 MountPath: "/tmp", 298 ReadOnly: false, 299 } 300 301 return v, vm 302 } 303 304 func oauthVolume(secret, key string) (coreapi.Volume, coreapi.VolumeMount) { 305 return coreapi.Volume{ 306 Name: secret, 307 VolumeSource: coreapi.VolumeSource{ 308 Secret: &coreapi.SecretVolumeSource{ 309 SecretName: secret, 310 Items: []coreapi.KeyToPath{{ 311 Key: key, 312 Path: fmt.Sprintf("./%s", key), 313 }}, 314 }, 315 }, 316 }, coreapi.VolumeMount{ 317 Name: secret, 318 MountPath: "/secrets/oauth", 319 ReadOnly: true, 320 } 321 } 322 323 func githubAppVolume(secret, key string) (coreapi.Volume, coreapi.VolumeMount) { 324 return coreapi.Volume{ 325 Name: secret, 326 VolumeSource: coreapi.VolumeSource{ 327 Secret: &coreapi.SecretVolumeSource{ 328 SecretName: secret, 329 Items: []coreapi.KeyToPath{{ 330 Key: key, 331 Path: fmt.Sprintf("./%s", key), 332 }}, 333 }, 334 }, 335 }, coreapi.VolumeMount{ 336 Name: secret, 337 MountPath: "/secrets/github-app", 338 ReadOnly: true, 339 } 340 } 341 342 // sshVolume converts a secret holding ssh keys into the corresponding volume and mount. 343 // 344 // This is used by CloneRefs to attach the mount to the clonerefs container. 345 func sshVolume(secret string) (coreapi.Volume, coreapi.VolumeMount) { 346 var sshKeyMode int32 = 0400 // this is octal, so symbolic ref is `u+r` 347 name := strings.Join([]string{"ssh-keys", secret}, "-") 348 mountPath := path.Join("/secrets/ssh", secret) 349 v := coreapi.Volume{ 350 Name: name, 351 VolumeSource: coreapi.VolumeSource{ 352 Secret: &coreapi.SecretVolumeSource{ 353 SecretName: secret, 354 DefaultMode: &sshKeyMode, 355 }, 356 }, 357 } 358 359 vm := coreapi.VolumeMount{ 360 Name: name, 361 MountPath: mountPath, 362 ReadOnly: true, 363 } 364 365 return v, vm 366 } 367 368 // cookiefileVolumes converts a secret holding cookies into the corresponding volume and mount. 369 // 370 // Secret can be of the form secret-name/base-name or just secret-name. 371 // Here secret-name refers to the kubernetes secret volume to mount, and base-name refers to the key in the secret 372 // where the cookies are stored. The secret-name pattern is equivalent to secret-name/secret-name. 373 // 374 // This is used by CloneRefs to attach the mount to the clonerefs container. 375 // The returned string value is the path to the cookiefile for use with --cookiefile. 376 func cookiefileVolume(secret string) (coreapi.Volume, coreapi.VolumeMount, string) { 377 // Separate secret-name/key-in-secret 378 parts := strings.SplitN(secret, "/", 2) 379 cookieSecret := parts[0] 380 var base string 381 if len(parts) == 1 { 382 base = parts[0] // Assume key-in-secret == secret-name 383 } else { 384 base = parts[1] 385 } 386 var cookiefileMode int32 = 0400 // u+r 387 vol := coreapi.Volume{ 388 Name: "cookiefile", 389 VolumeSource: coreapi.VolumeSource{ 390 Secret: &coreapi.SecretVolumeSource{ 391 SecretName: cookieSecret, 392 DefaultMode: &cookiefileMode, 393 }, 394 }, 395 } 396 mount := coreapi.VolumeMount{ 397 Name: vol.Name, 398 MountPath: "/secrets/cookiefile", // append base to flag 399 ReadOnly: true, 400 } 401 return vol, mount, path.Join(mount.MountPath, base) 402 } 403 404 // CloneRefs constructs the container and volumes necessary to clone the refs requested by the ProwJob. 405 // 406 // The container checks out repositories specified by the ProwJob Refs to `codeMount`. 407 // A log of what it checked out is written to `clone.json` in `logMount`. 408 // 409 // The container may need to mount SSH keys and/or cookiefiles in order to access private refs. 410 // CloneRefs returns a list of volumes containing these secrets required by the container. 411 func CloneRefs(pj prowapi.ProwJob, codeMount, logMount coreapi.VolumeMount) (*coreapi.Container, []prowapi.Refs, []coreapi.Volume, error) { 412 if pj.Spec.DecorationConfig == nil { 413 return nil, nil, nil, nil 414 } 415 if skip := pj.Spec.DecorationConfig.SkipCloning; skip != nil && *skip { 416 return nil, nil, nil, nil 417 } 418 var cloneVolumes []coreapi.Volume 419 var refs []prowapi.Refs // Do not return []*prowapi.Refs which we do not own 420 if pj.Spec.Refs != nil { 421 refs = append(refs, *pj.Spec.Refs) 422 } 423 refs = append(refs, pj.Spec.ExtraRefs...) 424 if len(refs) == 0 { // nothing to clone 425 return nil, nil, nil, nil 426 } 427 if codeMount.Name == "" || codeMount.MountPath == "" { 428 return nil, nil, nil, fmt.Errorf("codeMount must set Name and MountPath") 429 } 430 if logMount.Name == "" || logMount.MountPath == "" { 431 return nil, nil, nil, fmt.Errorf("logMount must set Name and MountPath") 432 } 433 434 var cloneMounts []coreapi.VolumeMount 435 var sshKeyPaths []string 436 for _, secret := range pj.Spec.DecorationConfig.SSHKeySecrets { 437 volume, mount := sshVolume(secret) 438 cloneMounts = append(cloneMounts, mount) 439 sshKeyPaths = append(sshKeyPaths, mount.MountPath) 440 cloneVolumes = append(cloneVolumes, volume) 441 } 442 443 var oauthMountPath string 444 if pj.Spec.DecorationConfig.OauthTokenSecret != nil { 445 oauthVolume, oauthMount := oauthVolume(pj.Spec.DecorationConfig.OauthTokenSecret.Name, pj.Spec.DecorationConfig.OauthTokenSecret.Key) 446 cloneMounts = append(cloneMounts, oauthMount) 447 oauthMountPath = filepath.Join(oauthMount.MountPath, pj.Spec.DecorationConfig.OauthTokenSecret.Key) 448 cloneVolumes = append(cloneVolumes, oauthVolume) 449 } 450 451 githubAPIEndpoints := pj.Spec.DecorationConfig.GitHubAPIEndpoints 452 if len(githubAPIEndpoints) == 0 { 453 githubAPIEndpoints = []string{github.DefaultAPIEndpoint} 454 } 455 456 var githubAppPrivateKeyMountPath string 457 if pj.Spec.DecorationConfig.GitHubAppPrivateKeySecret != nil { 458 keyVolume, keyMount := githubAppVolume(pj.Spec.DecorationConfig.GitHubAppPrivateKeySecret.Name, pj.Spec.DecorationConfig.GitHubAppPrivateKeySecret.Key) 459 cloneMounts = append(cloneMounts, keyMount) 460 githubAppPrivateKeyMountPath = filepath.Join(keyMount.MountPath, pj.Spec.DecorationConfig.GitHubAppPrivateKeySecret.Key) 461 cloneVolumes = append(cloneVolumes, keyVolume) 462 } 463 464 volume, mount := tmpVolume("clonerefs-tmp") 465 cloneMounts = append(cloneMounts, mount) 466 cloneVolumes = append(cloneVolumes, volume) 467 468 var cloneArgs []string 469 var cookiefilePath string 470 471 if cp := pj.Spec.DecorationConfig.CookiefileSecret; cp != nil && *cp != "" { 472 v, vm, vp := cookiefileVolume(*cp) 473 cloneMounts = append(cloneMounts, vm) 474 cloneVolumes = append(cloneVolumes, v) 475 cookiefilePath = vp 476 cloneArgs = append(cloneArgs, "--cookiefile="+cookiefilePath) 477 } 478 479 env, err := cloneEnv(clonerefs.Options{ 480 CookiePath: cookiefilePath, 481 GitRefs: refs, 482 GitUserEmail: clonerefs.DefaultGitUserEmail, 483 GitUserName: clonerefs.DefaultGitUserName, 484 HostFingerprints: pj.Spec.DecorationConfig.SSHHostFingerprints, 485 KeyFiles: sshKeyPaths, 486 Log: CloneLogPath(logMount), 487 SrcRoot: codeMount.MountPath, 488 OauthTokenFile: oauthMountPath, 489 GitHubAPIEndpoints: githubAPIEndpoints, 490 GitHubAppID: pj.Spec.DecorationConfig.GitHubAppID, 491 GitHubAppPrivateKeyFile: githubAppPrivateKeyMountPath, 492 }) 493 if err != nil { 494 return nil, nil, nil, fmt.Errorf("clone env: %w", err) 495 } 496 497 container := coreapi.Container{ 498 Name: cloneRefsName, 499 Image: pj.Spec.DecorationConfig.UtilityImages.CloneRefs, 500 Args: cloneArgs, 501 Env: env, 502 VolumeMounts: append([]coreapi.VolumeMount{logMount, codeMount}, cloneMounts...), 503 } 504 505 if pj.Spec.DecorationConfig.Resources != nil && pj.Spec.DecorationConfig.Resources.CloneRefs != nil { 506 container.Resources = *pj.Spec.DecorationConfig.Resources.CloneRefs 507 } 508 return &container, refs, cloneVolumes, nil 509 } 510 511 func processLog(log coreapi.VolumeMount, prefix string) string { 512 if prefix == "" { 513 return filepath.Join(log.MountPath, "process-log.txt") 514 } 515 return filepath.Join(log.MountPath, fmt.Sprintf("%s-log.txt", prefix)) 516 } 517 518 func markerFile(log coreapi.VolumeMount, prefix string) string { 519 if prefix == "" { 520 return filepath.Join(log.MountPath, "marker-file.txt") 521 } 522 return filepath.Join(log.MountPath, fmt.Sprintf("%s-marker.txt", prefix)) 523 } 524 525 func metadataFile(log coreapi.VolumeMount, prefix string) string { 526 ad := artifactsDir(log) 527 if prefix == "" { 528 return filepath.Join(ad, "metadata.json") 529 } 530 return filepath.Join(ad, fmt.Sprintf("%s-metadata.json", prefix)) 531 } 532 533 func artifactsDir(log coreapi.VolumeMount) string { 534 return filepath.Join(log.MountPath, "artifacts") 535 } 536 537 func entrypointLocation(tools coreapi.VolumeMount) string { 538 return filepath.Join(tools.MountPath, "entrypoint") 539 } 540 541 // InjectEntrypoint will make the entrypoint binary in the tools volume the container's entrypoint, which will output to the log volume. 542 func InjectEntrypoint(c *coreapi.Container, timeout, gracePeriod time.Duration, prefix, previousMarker string, propagateErrorCode bool, exitZero bool, log, tools coreapi.VolumeMount) (*wrapper.Options, error) { 543 wrapperOptions := &wrapper.Options{ 544 Args: append(c.Command, c.Args...), 545 ContainerName: c.Name, 546 ProcessLog: processLog(log, prefix), 547 MarkerFile: markerFile(log, prefix), 548 MetadataFile: metadataFile(log, prefix), 549 } 550 // TODO(fejta): use flags 551 entrypointConfigEnv, err := entrypoint.Encode(entrypoint.Options{ 552 ArtifactDir: artifactsDir(log), 553 GracePeriod: gracePeriod, 554 Options: wrapperOptions, 555 Timeout: timeout, 556 PropagateErrorCode: propagateErrorCode, 557 AlwaysZero: exitZero, 558 PreviousMarker: previousMarker, 559 }) 560 if err != nil { 561 return nil, err 562 } 563 564 c.Command = []string{entrypointLocation(tools)} 565 c.Args = nil 566 c.Env = append(c.Env, KubeEnv(map[string]string{entrypoint.JSONConfigEnvVar: entrypointConfigEnv})...) 567 c.VolumeMounts = append(c.VolumeMounts, log, tools) 568 return wrapperOptions, nil 569 } 570 571 // PlaceEntrypoint will copy entrypoint from the entrypoint image to the tools volume 572 func PlaceEntrypoint(config *prowapi.DecorationConfig, toolsMount coreapi.VolumeMount) coreapi.Container { 573 container := coreapi.Container{ 574 Name: entrypointName, 575 Image: config.UtilityImages.Entrypoint, 576 Args: []string{"--copy-mode-only"}, 577 VolumeMounts: []coreapi.VolumeMount{toolsMount}, 578 } 579 if config.Resources != nil && config.Resources.PlaceEntrypoint != nil { 580 container.Resources = *config.Resources.PlaceEntrypoint 581 } 582 return container 583 } 584 585 func BlobStorageOptions(dc prowapi.DecorationConfig, localMode bool) ([]coreapi.Volume, []coreapi.VolumeMount, gcsupload.Options) { 586 opt := gcsupload.Options{ 587 // TODO: pass the artifact dir here too once we figure that out 588 GCSConfiguration: dc.GCSConfiguration, 589 DryRun: false, 590 } 591 if localMode { 592 opt.LocalOutputDir = outputMountPath 593 // The GCS credentials are not needed for local mode. 594 return nil, nil, opt 595 } 596 597 var volumes []coreapi.Volume 598 var mounts []coreapi.VolumeMount 599 if dc.GCSCredentialsSecret != nil && *dc.GCSCredentialsSecret != "" { 600 volumes = append(volumes, coreapi.Volume{ 601 Name: gcsCredentialsMountName, 602 VolumeSource: coreapi.VolumeSource{ 603 Secret: &coreapi.SecretVolumeSource{ 604 SecretName: *dc.GCSCredentialsSecret, 605 }, 606 }, 607 }) 608 mounts = append(mounts, coreapi.VolumeMount{ 609 Name: gcsCredentialsMountName, 610 MountPath: gcsCredentialsMountPath, 611 }) 612 opt.StorageClientOptions.GCSCredentialsFile = fmt.Sprintf("%s/service-account.json", gcsCredentialsMountPath) 613 } 614 if dc.S3CredentialsSecret != nil && *dc.S3CredentialsSecret != "" { 615 volumes = append(volumes, coreapi.Volume{ 616 Name: s3CredentialsMountName, 617 VolumeSource: coreapi.VolumeSource{ 618 Secret: &coreapi.SecretVolumeSource{ 619 SecretName: *dc.S3CredentialsSecret, 620 }, 621 }, 622 }) 623 mounts = append(mounts, coreapi.VolumeMount{ 624 Name: s3CredentialsMountName, 625 MountPath: s3CredentialsMountPath, 626 }) 627 opt.StorageClientOptions.S3CredentialsFile = fmt.Sprintf("%s/service-account.json", s3CredentialsMountPath) 628 } 629 630 return volumes, mounts, opt 631 } 632 633 func InitUpload(config *prowapi.DecorationConfig, gcsOptions gcsupload.Options, blobStorageMounts []coreapi.VolumeMount, cloneLogMount *coreapi.VolumeMount, outputMount *coreapi.VolumeMount, encodedJobSpec string) (*coreapi.Container, error) { 634 // TODO(fejta): remove encodedJobSpec 635 initUploadOptions := initupload.Options{ 636 Options: &gcsOptions, 637 } 638 var mounts []coreapi.VolumeMount 639 if cloneLogMount != nil { 640 initUploadOptions.Log = CloneLogPath(*cloneLogMount) 641 mounts = append(mounts, *cloneLogMount) 642 } 643 mounts = append(mounts, blobStorageMounts...) 644 if outputMount != nil { 645 mounts = append(mounts, *outputMount) 646 } 647 // TODO(fejta): use flags 648 initUploadConfigEnv, err := initupload.Encode(initUploadOptions) 649 if err != nil { 650 return nil, fmt.Errorf("could not encode initupload configuration as JSON: %w", err) 651 } 652 container := &coreapi.Container{ 653 Name: initUploadName, 654 Image: config.UtilityImages.InitUpload, 655 Env: KubeEnv(map[string]string{ 656 downwardapi.JobSpecEnv: encodedJobSpec, 657 initupload.JSONConfigEnvVar: initUploadConfigEnv, 658 }), 659 VolumeMounts: mounts, 660 } 661 if config.Resources != nil && config.Resources.InitUpload != nil { 662 container.Resources = *config.Resources.InitUpload 663 } 664 return container, nil 665 } 666 667 // LogMountAndVolume returns the canonical volume and mount used to persist container logs. 668 func LogMountAndVolume() (coreapi.VolumeMount, coreapi.Volume) { 669 return coreapi.VolumeMount{ 670 Name: logMountName, 671 MountPath: logMountPath, 672 }, coreapi.Volume{ 673 Name: logMountName, 674 VolumeSource: coreapi.VolumeSource{ 675 EmptyDir: &coreapi.EmptyDirVolumeSource{}, 676 }, 677 } 678 } 679 680 // CodeMountAndVolume returns the canonical volume and mount used to share code under test 681 func CodeMountAndVolume() (coreapi.VolumeMount, coreapi.Volume) { 682 return coreapi.VolumeMount{ 683 Name: codeMountName, 684 MountPath: codeMountPath, 685 }, coreapi.Volume{ 686 Name: codeMountName, 687 VolumeSource: coreapi.VolumeSource{ 688 EmptyDir: &coreapi.EmptyDirVolumeSource{}, 689 }, 690 } 691 } 692 693 // ToolsMountAndVolume returns the canonical volume and mount used to propagate the entrypoint 694 func ToolsMountAndVolume() (coreapi.VolumeMount, coreapi.Volume) { 695 return coreapi.VolumeMount{ 696 Name: toolsMountName, 697 MountPath: toolsMountPath, 698 }, coreapi.Volume{ 699 Name: toolsMountName, 700 VolumeSource: coreapi.VolumeSource{ 701 EmptyDir: &coreapi.EmptyDirVolumeSource{}, 702 }, 703 } 704 } 705 706 func decorate(spec *coreapi.PodSpec, pj *prowapi.ProwJob, rawEnv map[string]string, outputDir string) error { 707 // TODO(fejta): we should pass around volume names rather than forcing particular mount paths. 708 709 rawEnv[artifactsEnv] = artifactsPath 710 rawEnv[gopathEnv] = codeMountPath // TODO(fejta): remove this once we can assume go modules 711 logMount, logVolume := LogMountAndVolume() 712 codeMount, codeVolume := CodeMountAndVolume() 713 toolsMount, toolsVolume := ToolsMountAndVolume() 714 715 // The output volume is only used if outputDir is specified, indicating the pod-utils should 716 // copy files instead of uploading to GCS. 717 localMode := outputDir != "" 718 var outputMount *coreapi.VolumeMount 719 var outputVolume *coreapi.Volume 720 if localMode { 721 outputMount = &coreapi.VolumeMount{ 722 Name: outputMountName, 723 MountPath: outputMountPath, 724 } 725 outputVolume = &coreapi.Volume{ 726 Name: outputMountName, 727 VolumeSource: coreapi.VolumeSource{ 728 HostPath: &coreapi.HostPathVolumeSource{ 729 Path: outputDir, 730 }, 731 }, 732 } 733 } 734 735 blobStorageVolumes, blobStorageMounts, blobStorageOptions := BlobStorageOptions(*pj.Spec.DecorationConfig, localMode) 736 737 cloner, refs, cloneVolumes, err := CloneRefs(*pj, codeMount, logMount) 738 if err != nil { 739 return fmt.Errorf("create clonerefs container: %w", err) 740 } 741 var cloneLogMount *coreapi.VolumeMount 742 if cloner != nil { 743 spec.InitContainers = append([]coreapi.Container{*cloner}, spec.InitContainers...) 744 cloneLogMount = &logMount 745 } 746 747 encodedJobSpec := rawEnv[downwardapi.JobSpecEnv] 748 initUpload, err := InitUpload(pj.Spec.DecorationConfig, blobStorageOptions, blobStorageMounts, cloneLogMount, outputMount, encodedJobSpec) 749 if err != nil { 750 return fmt.Errorf("create initupload container: %w", err) 751 } 752 spec.InitContainers = append( 753 spec.InitContainers, 754 *initUpload, 755 PlaceEntrypoint(pj.Spec.DecorationConfig, toolsMount), 756 ) 757 for i, container := range spec.Containers { 758 spec.Containers[i].Env = append(container.Env, KubeEnv(rawEnv)...) 759 } 760 761 secretVolumes := sets.New[string]() 762 for _, volume := range spec.Volumes { 763 if volume.VolumeSource.Secret != nil { 764 secretVolumes.Insert(volume.Name) 765 } 766 } 767 containsSecretData := func(volumeName string) bool { 768 if censor := pj.Spec.DecorationConfig.CensorSecrets; censor == nil || !*censor { 769 return false 770 } 771 return secretVolumes.Has(volumeName) 772 } 773 774 const ( 775 previous = "" 776 exitZero = false 777 propagateErrorCode = false 778 ) 779 var secretVolumeMounts []coreapi.VolumeMount 780 var wrappers []wrapper.Options 781 782 for i, container := range spec.Containers { 783 prefix := container.Name 784 if len(spec.Containers) == 1 { 785 prefix = "" 786 } 787 wrapperOptions, err := InjectEntrypoint(&spec.Containers[i], pj.Spec.DecorationConfig.Timeout.Get(), pj.Spec.DecorationConfig.GracePeriod.Get(), prefix, previous, propagateErrorCode, exitZero, logMount, toolsMount) 788 if err != nil { 789 return fmt.Errorf("wrap container: %w", err) 790 } 791 for _, volumeMount := range spec.Containers[i].VolumeMounts { 792 if containsSecretData(volumeMount.Name) { 793 secretVolumeMounts = append(secretVolumeMounts, volumeMount) 794 } 795 } 796 wrappers = append(wrappers, *wrapperOptions) 797 } 798 799 ignoreInterrupts := pj.Spec.DecorationConfig.UploadIgnoresInterrupts != nil && *pj.Spec.DecorationConfig.UploadIgnoresInterrupts 800 801 sidecar, err := Sidecar(pj.Spec.DecorationConfig, blobStorageOptions, blobStorageMounts, logMount, outputMount, encodedJobSpec, !RequirePassingEntries, ignoreInterrupts, secretVolumeMounts, wrappers...) 802 if err != nil { 803 return fmt.Errorf("create sidecar: %w", err) 804 } 805 806 spec.Volumes = append(spec.Volumes, logVolume, toolsVolume) 807 spec.Volumes = append(spec.Volumes, blobStorageVolumes...) 808 if outputVolume != nil { 809 spec.Volumes = append(spec.Volumes, *outputVolume) 810 } 811 812 if len(refs) > 0 { 813 for i, container := range spec.Containers { 814 spec.Containers[i].WorkingDir = DetermineWorkDir(codeMount.MountPath, refs) 815 spec.Containers[i].VolumeMounts = append(container.VolumeMounts, codeMount) 816 } 817 spec.Volumes = append(spec.Volumes, append(cloneVolumes, codeVolume)...) 818 } 819 820 if pj.Spec.DecorationConfig != nil && pj.Spec.DecorationConfig.DefaultMemoryRequest != nil { 821 for i, container := range spec.Containers { 822 if container.Resources.Requests != nil { 823 if _, ok := container.Resources.Requests[coreapi.ResourceMemory]; ok { 824 continue // Memory request already defined, no need to default 825 } 826 } 827 if spec.Containers[i].Resources.Requests == nil { 828 spec.Containers[i].Resources.Requests = make(coreapi.ResourceList) 829 } 830 spec.Containers[i].Resources.Requests[coreapi.ResourceMemory] = *pj.Spec.DecorationConfig.DefaultMemoryRequest 831 } 832 } 833 834 if pj.Spec.DecorationConfig != nil { 835 if spec.SecurityContext == nil { 836 spec.SecurityContext = new(coreapi.PodSecurityContext) 837 } 838 if pj.Spec.DecorationConfig.RunAsUser != nil && spec.SecurityContext.RunAsUser == nil { 839 spec.SecurityContext.RunAsUser = pj.Spec.DecorationConfig.RunAsUser 840 } 841 if pj.Spec.DecorationConfig.RunAsGroup != nil && spec.SecurityContext.RunAsGroup == nil { 842 spec.SecurityContext.RunAsGroup = pj.Spec.DecorationConfig.RunAsGroup 843 } 844 if pj.Spec.DecorationConfig.FsGroup != nil && spec.SecurityContext.FSGroup == nil { 845 spec.SecurityContext.FSGroup = pj.Spec.DecorationConfig.FsGroup 846 } 847 } 848 849 if pj.Spec.DecorationConfig != nil && pj.Spec.DecorationConfig.SetLimitEqualsMemoryRequest != nil && *pj.Spec.DecorationConfig.SetLimitEqualsMemoryRequest { 850 for i, container := range spec.Containers { 851 if container.Resources.Requests == nil { 852 continue 853 } 854 if val, ok := container.Resources.Requests[coreapi.ResourceMemory]; ok { 855 if spec.Containers[i].Resources.Limits == nil { 856 spec.Containers[i].Resources.Limits = make(coreapi.ResourceList) 857 } 858 spec.Containers[i].Resources.Limits[coreapi.ResourceMemory] = val 859 } 860 } 861 } 862 863 spec.Containers = append(spec.Containers, *sidecar) 864 865 if spec.TerminationGracePeriodSeconds == nil && pj.Spec.DecorationConfig.GracePeriod != nil { 866 // Unless the user's asked for something specific, we want to set the grace period on the Pod to 867 // a reasonable value, as the overall grace period for the Pod must encompass both the time taken 868 // to gracefully terminate the test process *and* the time taken to process and upload the resulting 869 // artifacts to the cloud. As a reasonable rule of thumb, assume a 80/20 split between these tasks. 870 gracePeriodSeconds := int64(pj.Spec.DecorationConfig.GracePeriod.Seconds()) * 5 / 4 871 spec.TerminationGracePeriodSeconds = &gracePeriodSeconds 872 } 873 874 defaultSA := pj.Spec.DecorationConfig.DefaultServiceAccountName 875 if spec.ServiceAccountName == "" && defaultSA != nil { 876 spec.ServiceAccountName = *defaultSA 877 } 878 879 return nil 880 } 881 882 // DetermineWorkDir determines the working directory to use for a given set of refs to clone 883 func DetermineWorkDir(baseDir string, refs []prowapi.Refs) string { 884 for _, ref := range refs { 885 if ref.WorkDir { 886 return clone.PathForRefs(baseDir, ref) 887 } 888 } 889 return clone.PathForRefs(baseDir, refs[0]) 890 } 891 892 const ( 893 // RequirePassingEntries causes sidecar to return an error if any entry fails. Otherwise it exits cleanly so long as it can complete. 894 RequirePassingEntries = true 895 ) 896 897 func Sidecar(config *prowapi.DecorationConfig, gcsOptions gcsupload.Options, blobStorageMounts []coreapi.VolumeMount, logMount coreapi.VolumeMount, outputMount *coreapi.VolumeMount, encodedJobSpec string, requirePassingEntries, ignoreInterrupts bool, secretVolumeMounts []coreapi.VolumeMount, wrappers ...wrapper.Options) (*coreapi.Container, error) { 898 var secretVolumePaths []string 899 for _, volumeMount := range secretVolumeMounts { 900 secretVolumePaths = append(secretVolumePaths, volumeMount.MountPath) 901 } 902 gcsOptions.Items = append(gcsOptions.Items, artifactsDir(logMount)) 903 censoringOptions := &sidecar.CensoringOptions{ 904 SecretDirectories: secretVolumePaths, 905 } 906 if config.CensoringOptions != nil { 907 censoringOptions.CensoringConcurrency = config.CensoringOptions.CensoringConcurrency 908 censoringOptions.CensoringBufferSize = config.CensoringOptions.CensoringBufferSize 909 censoringOptions.IncludeDirectories = config.CensoringOptions.IncludeDirectories 910 censoringOptions.ExcludeDirectories = config.CensoringOptions.ExcludeDirectories 911 } 912 sidecarConfigEnv, err := sidecar.Encode(sidecar.Options{ 913 GcsOptions: &gcsOptions, 914 Entries: wrappers, 915 EntryError: requirePassingEntries, 916 IgnoreInterrupts: ignoreInterrupts, 917 CensoringOptions: censoringOptions, 918 }) 919 920 if err != nil { 921 return nil, err 922 } 923 mounts := []coreapi.VolumeMount{logMount} 924 mounts = append(mounts, blobStorageMounts...) 925 mounts = append(mounts, secretVolumeMounts...) 926 if outputMount != nil { 927 mounts = append(mounts, *outputMount) 928 } 929 930 container := &coreapi.Container{ 931 Name: sidecarName, 932 Image: config.UtilityImages.Sidecar, 933 Env: KubeEnv(map[string]string{ 934 sidecar.JSONConfigEnvVar: sidecarConfigEnv, 935 downwardapi.JobSpecEnv: encodedJobSpec, // TODO: shouldn't need this? 936 }), 937 VolumeMounts: mounts, 938 TerminationMessagePolicy: coreapi.TerminationMessageFallbackToLogsOnError, 939 } 940 if config.Resources != nil && config.Resources.Sidecar != nil { 941 container.Resources = *config.Resources.Sidecar 942 } 943 return container, nil 944 } 945 946 // KubeEnv transforms a mapping of environment variables 947 // into their serialized form for a PodSpec, sorting by 948 // the name of the env vars 949 func KubeEnv(environment map[string]string) []coreapi.EnvVar { 950 var keys []string 951 for key := range environment { 952 keys = append(keys, key) 953 } 954 sort.Strings(keys) 955 956 var kubeEnvironment []coreapi.EnvVar 957 for _, key := range keys { 958 kubeEnvironment = append(kubeEnvironment, coreapi.EnvVar{ 959 Name: key, 960 Value: environment[key], 961 }) 962 } 963 964 return kubeEnvironment 965 }