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  }