github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/pjutil/pjutil.go (about)

     1  /*
     2  Copyright 2017 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 pjutil contains helpers for working with ProwJobs.
    18  package pjutil
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"net/url"
    24  	"path"
    25  	"strconv"
    26  
    27  	uuid "github.com/google/uuid"
    28  	"github.com/sirupsen/logrus"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  
    31  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    32  	"sigs.k8s.io/prow/pkg/config"
    33  	"sigs.k8s.io/prow/pkg/gcsupload"
    34  	"sigs.k8s.io/prow/pkg/github"
    35  	"sigs.k8s.io/prow/pkg/kube"
    36  	"sigs.k8s.io/prow/pkg/pod-utils/decorate"
    37  	"sigs.k8s.io/prow/pkg/pod-utils/downwardapi"
    38  )
    39  
    40  // Modifiers allows a client to set some fields
    41  // when a ProwJob is being created.
    42  type Modifiers struct {
    43  	state prowapi.ProwJobState
    44  }
    45  
    46  // Modifier configures a Modifiers value
    47  type Modifier func(*Modifiers)
    48  
    49  func defaultModifiers() Modifiers {
    50  	return Modifiers{state: prowapi.TriggeredState}
    51  }
    52  
    53  // RequireScheduling returns an Option that, if enabled, set
    54  // the ProwJob initial state to SchedulingState
    55  func RequireScheduling(enableScheduling bool) Modifier {
    56  	if enableScheduling {
    57  		return func(opts *Modifiers) { opts.state = prowapi.SchedulingState }
    58  	}
    59  	return func(*Modifiers) {}
    60  }
    61  
    62  // NewProwJob initializes a ProwJob out of a ProwJobSpec, with some extra modifiers.
    63  func NewProwJob(spec prowapi.ProwJobSpec, extraLabels, extraAnnotations map[string]string, modifiers ...Modifier) prowapi.ProwJob {
    64  	labels, annotations := decorate.LabelsAndAnnotationsForSpec(spec, extraLabels, extraAnnotations)
    65  	specCopy := spec.DeepCopy()
    66  	setReportDefault(specCopy)
    67  	uuidV1 := uuid.New()
    68  
    69  	pj := prowapi.ProwJob{
    70  		TypeMeta: metav1.TypeMeta{
    71  			APIVersion: "prow.k8s.io/v1",
    72  			Kind:       "ProwJob",
    73  		},
    74  		ObjectMeta: metav1.ObjectMeta{
    75  			Name:        uuidV1.String(),
    76  			Labels:      labels,
    77  			Annotations: annotations,
    78  		},
    79  		Spec: *specCopy,
    80  		Status: prowapi.ProwJobStatus{
    81  			StartTime: metav1.Now(),
    82  			State:     prowapi.TriggeredState,
    83  		},
    84  	}
    85  
    86  	defModifiers := defaultModifiers()
    87  	for _, modifier := range modifiers {
    88  		modifier(&defModifiers)
    89  	}
    90  	pj.Status.State = defModifiers.state
    91  
    92  	return pj
    93  }
    94  
    95  // setReportDefault sets Slack to false when states to report is an empty slice.
    96  //
    97  // `omitempty` is required for fields that are optional, otherwise strict prowjob CRD
    98  // validation will reject prowjob without the field.
    99  // For `ReporterConfig.Slack.JobStatesToReport`, it's certainly not a required field
   100  // for all prowjobs.
   101  // However, when omitempty is presented, `JobStatesToReport: []` roundtrip into
   102  // `JobStatesToReport: nil`, which prevents a prowjob from overriding global Slack
   103  // report configuration(ref: https://github.com/kubernetes/test-infra/issues/22888#issuecomment-881513368).
   104  // Use a boolean instead so that we can use `omitempty`.
   105  // `report: false` has highest priority when it comes to decide whether
   106  // this job should report or not. `false` strictly means not, which could be
   107  // resulted from either of the following config:
   108  // - `job_state_to_report: []`
   109  // - `report: false`
   110  // `report: true` also depends on other conditions, such as channel name etc.
   111  func setReportDefault(spec *prowapi.ProwJobSpec) {
   112  	if spec.ReporterConfig == nil || spec.ReporterConfig.Slack == nil {
   113  		return
   114  	}
   115  	// `job_states_to_report: []` means false
   116  	if spec.ReporterConfig.Slack.JobStatesToReport != nil && len(spec.ReporterConfig.Slack.JobStatesToReport) == 0 {
   117  		spec.ReporterConfig.Slack.Report = boolPtr(false)
   118  	} else {
   119  		spec.ReporterConfig.Slack.Report = boolPtr(true)
   120  	}
   121  }
   122  
   123  func createRefs(pr github.PullRequest, baseSHA string) prowapi.Refs {
   124  	org := pr.Base.Repo.Owner.Login
   125  	repo := pr.Base.Repo.Name
   126  	repoLink := pr.Base.Repo.HTMLURL
   127  	number := pr.Number
   128  	return prowapi.Refs{
   129  		Org:      org,
   130  		Repo:     repo,
   131  		RepoLink: repoLink,
   132  		BaseRef:  pr.Base.Ref,
   133  		BaseSHA:  baseSHA,
   134  		BaseLink: fmt.Sprintf("%s/commit/%s", repoLink, baseSHA),
   135  		Pulls: []prowapi.Pull{
   136  			{
   137  				Number:     number,
   138  				Author:     pr.User.Login,
   139  				SHA:        pr.Head.SHA,
   140  				HeadRef:    pr.Head.Ref,
   141  				Title:      pr.Title,
   142  				Link:       pr.HTMLURL,
   143  				AuthorLink: pr.User.HTMLURL,
   144  				CommitLink: fmt.Sprintf("%s/pull/%d/commits/%s", repoLink, number, pr.Head.SHA),
   145  			},
   146  		},
   147  	}
   148  }
   149  
   150  // NewPresubmit converts a config.Presubmit into a prowapi.ProwJob.
   151  // The prowapi.Refs are configured correctly per the pr, baseSHA.
   152  // The eventGUID becomes a github.EventGUID label.
   153  // Presubmit is finally mutated according to the modifiers.
   154  func NewPresubmit(pr github.PullRequest, baseSHA string, job config.Presubmit, eventGUID string, additionalLabels map[string]string, modifiers ...Modifier) prowapi.ProwJob {
   155  	refs := createRefs(pr, baseSHA)
   156  	labels := make(map[string]string)
   157  	for k, v := range job.Labels {
   158  		labels[k] = v
   159  	}
   160  	for k, v := range additionalLabels {
   161  		labels[k] = v
   162  	}
   163  	labels[github.EventGUID] = eventGUID
   164  	labels[kube.IsOptionalLabel] = strconv.FormatBool(job.Optional)
   165  	annotations := make(map[string]string)
   166  	for k, v := range job.Annotations {
   167  		annotations[k] = v
   168  	}
   169  	return NewProwJob(PresubmitSpec(job, refs), labels, annotations, modifiers...)
   170  }
   171  
   172  // PresubmitSpec initializes a ProwJobSpec for a given presubmit job.
   173  func PresubmitSpec(p config.Presubmit, refs prowapi.Refs) prowapi.ProwJobSpec {
   174  	pjs := specFromJobBase(p.JobBase)
   175  	pjs.Type = prowapi.PresubmitJob
   176  	pjs.Context = p.Context
   177  	pjs.Report = !p.SkipReport
   178  	pjs.RerunCommand = p.RerunCommand
   179  	if p.JenkinsSpec != nil {
   180  		pjs.JenkinsSpec = &prowapi.JenkinsSpec{
   181  			GitHubBranchSourceJob: p.JenkinsSpec.GitHubBranchSourceJob,
   182  		}
   183  	}
   184  	pjs.Refs = CompletePrimaryRefs(refs, p.JobBase)
   185  
   186  	return pjs
   187  }
   188  
   189  // PostsubmitSpec initializes a ProwJobSpec for a given postsubmit job.
   190  func PostsubmitSpec(p config.Postsubmit, refs prowapi.Refs) prowapi.ProwJobSpec {
   191  	pjs := specFromJobBase(p.JobBase)
   192  	pjs.Type = prowapi.PostsubmitJob
   193  	pjs.Context = p.Context
   194  	pjs.Report = !p.SkipReport
   195  	pjs.Refs = CompletePrimaryRefs(refs, p.JobBase)
   196  	if p.JenkinsSpec != nil {
   197  		pjs.JenkinsSpec = &prowapi.JenkinsSpec{
   198  			GitHubBranchSourceJob: p.JenkinsSpec.GitHubBranchSourceJob,
   199  		}
   200  	}
   201  
   202  	return pjs
   203  }
   204  
   205  // PeriodicSpec initializes a ProwJobSpec for a given periodic job.
   206  func PeriodicSpec(p config.Periodic) prowapi.ProwJobSpec {
   207  	pjs := specFromJobBase(p.JobBase)
   208  	// It is currently not possible to disable reporting for individual periodics.
   209  	pjs.Report = true
   210  	pjs.Type = prowapi.PeriodicJob
   211  
   212  	return pjs
   213  }
   214  
   215  // BatchSpec initializes a ProwJobSpec for a given batch job and ref spec.
   216  func BatchSpec(p config.Presubmit, refs prowapi.Refs) prowapi.ProwJobSpec {
   217  	pjs := specFromJobBase(p.JobBase)
   218  	pjs.Type = prowapi.BatchJob
   219  	pjs.Context = p.Context
   220  	pjs.Refs = CompletePrimaryRefs(refs, p.JobBase)
   221  
   222  	return pjs
   223  }
   224  
   225  func specFromJobBase(jb config.JobBase) prowapi.ProwJobSpec {
   226  	var namespace string
   227  	if jb.Namespace != nil {
   228  		namespace = *jb.Namespace
   229  	}
   230  	return prowapi.ProwJobSpec{
   231  		Job:             jb.Name,
   232  		Agent:           prowapi.ProwJobAgent(jb.Agent),
   233  		Cluster:         jb.Cluster,
   234  		Namespace:       namespace,
   235  		MaxConcurrency:  jb.MaxConcurrency,
   236  		ErrorOnEviction: jb.ErrorOnEviction,
   237  
   238  		ExtraRefs:        DecorateExtraRefs(jb.ExtraRefs, jb),
   239  		DecorationConfig: jb.DecorationConfig,
   240  
   241  		PodSpec:               jb.Spec,
   242  		PipelineRunSpec:       jb.PipelineRunSpec,
   243  		TektonPipelineRunSpec: jb.TektonPipelineRunSpec,
   244  
   245  		ReporterConfig:  jb.ReporterConfig,
   246  		RerunAuthConfig: jb.RerunAuthConfig,
   247  		Hidden:          jb.Hidden,
   248  		ProwJobDefault:  jb.ProwJobDefault,
   249  		JobQueueName:    jb.JobQueueName,
   250  	}
   251  }
   252  
   253  func DecorateExtraRefs(refs []prowapi.Refs, jb config.JobBase) []prowapi.Refs {
   254  	if jb.DecorationConfig == nil {
   255  		return refs
   256  	}
   257  	var rs []prowapi.Refs
   258  	for _, r := range refs {
   259  		rs = append(rs, *DecorateRefs(r, jb))
   260  	}
   261  	return rs
   262  }
   263  
   264  func DecorateRefs(refs prowapi.Refs, jb config.JobBase) *prowapi.Refs {
   265  	dc := jb.DecorationConfig
   266  	if dc == nil {
   267  		return &refs
   268  	}
   269  	if refs.BloblessFetch == nil {
   270  		refs.BloblessFetch = dc.BloblessFetch
   271  	}
   272  	return &refs
   273  }
   274  
   275  func CompletePrimaryRefs(refs prowapi.Refs, jb config.JobBase) *prowapi.Refs {
   276  	if jb.PathAlias != "" {
   277  		refs.PathAlias = jb.PathAlias
   278  	}
   279  	if jb.CloneURI != "" {
   280  		refs.CloneURI = jb.CloneURI
   281  	}
   282  	if jb.SkipSubmodules {
   283  		refs.SkipSubmodules = jb.SkipSubmodules
   284  	}
   285  	if jb.CloneDepth > 0 {
   286  		refs.CloneDepth = jb.CloneDepth
   287  	}
   288  	if jb.SkipFetchHead {
   289  		refs.SkipFetchHead = jb.SkipFetchHead
   290  	}
   291  	return DecorateRefs(refs, jb)
   292  }
   293  
   294  // PartitionActive separates the provided prowjobs into pending and triggered
   295  // and returns them inside channels so that they can be consumed in parallel
   296  // by different goroutines. Complete prowjobs are filtered out. Controller
   297  // loops need to handle pending jobs first so they can conform to maximum
   298  // concurrency requirements that different jobs may have.
   299  func PartitionActive(pjs []prowapi.ProwJob) (pending, triggered, aborted chan prowapi.ProwJob) {
   300  	// Size channels correctly.
   301  	pendingCount, triggeredCount, abortedCount := 0, 0, 0
   302  	for _, pj := range pjs {
   303  		switch pj.Status.State {
   304  		case prowapi.PendingState:
   305  			pendingCount++
   306  		case prowapi.TriggeredState:
   307  			triggeredCount++
   308  		case prowapi.AbortedState:
   309  			abortedCount++
   310  		}
   311  	}
   312  	pending = make(chan prowapi.ProwJob, pendingCount)
   313  	triggered = make(chan prowapi.ProwJob, triggeredCount)
   314  	aborted = make(chan prowapi.ProwJob, abortedCount)
   315  
   316  	// Partition the jobs into the two separate channels.
   317  	for _, pj := range pjs {
   318  		switch pj.Status.State {
   319  		case prowapi.PendingState:
   320  			pending <- pj
   321  		case prowapi.TriggeredState:
   322  			triggered <- pj
   323  		case prowapi.AbortedState:
   324  			if !pj.Complete() {
   325  				aborted <- pj
   326  			}
   327  		}
   328  	}
   329  	close(pending)
   330  	close(triggered)
   331  	close(aborted)
   332  	return pending, triggered, aborted
   333  }
   334  
   335  // GetLatestProwJobs filters through the provided prowjobs and returns
   336  // a map of jobType jobs to their latest prowjobs.
   337  func GetLatestProwJobs(pjs []prowapi.ProwJob, jobType prowapi.ProwJobType) map[string]prowapi.ProwJob {
   338  	latestJobs := make(map[string]prowapi.ProwJob)
   339  	for _, j := range pjs {
   340  		if j.Spec.Type != jobType {
   341  			continue
   342  		}
   343  		name := j.Spec.Job
   344  		if j.Status.StartTime.After(latestJobs[name].Status.StartTime.Time) {
   345  			latestJobs[name] = j
   346  		}
   347  	}
   348  	return latestJobs
   349  }
   350  
   351  // ProwJobFields extracts logrus fields from a prowjob useful for logging.
   352  func ProwJobFields(pj *prowapi.ProwJob) logrus.Fields {
   353  	fields := make(logrus.Fields)
   354  	fields["name"] = pj.ObjectMeta.Name
   355  	fields["job"] = pj.Spec.Job
   356  	fields["type"] = pj.Spec.Type
   357  	fields["state"] = pj.Status.State
   358  	if len(pj.ObjectMeta.Labels[github.EventGUID]) > 0 {
   359  		fields[github.EventGUID] = pj.ObjectMeta.Labels[github.EventGUID]
   360  	}
   361  	if pj.Spec.Refs != nil && len(pj.Spec.Refs.Pulls) == 1 {
   362  		fields[github.PrLogField] = pj.Spec.Refs.Pulls[0].Number
   363  		fields[github.RepoLogField] = pj.Spec.Refs.Repo
   364  		fields[github.OrgLogField] = pj.Spec.Refs.Org
   365  	}
   366  	if pj.Spec.JenkinsSpec != nil {
   367  		fields["github_based_job"] = pj.Spec.JenkinsSpec.GitHubBranchSourceJob
   368  	}
   369  
   370  	return fields
   371  }
   372  
   373  // JobURL returns the expected URL for ProwJobStatus.
   374  //
   375  // TODO(fejta): consider moving default JobURLTemplate and JobURLPrefix out of plank
   376  func JobURL(plank config.Plank, pj prowapi.ProwJob, log *logrus.Entry) (string, error) {
   377  	if pj.Spec.DecorationConfig != nil && plank.GetJobURLPrefix(&pj) != "" {
   378  		spec := downwardapi.NewJobSpec(pj.Spec, pj.Status.BuildID, pj.Name)
   379  		gcsConfig := pj.Spec.DecorationConfig.GCSConfiguration
   380  		_, gcsPath, _ := gcsupload.PathsForJob(gcsConfig, &spec, "")
   381  
   382  		prefix, _ := url.Parse(plank.GetJobURLPrefix(&pj))
   383  
   384  		prowPath, err := prowapi.ParsePath(gcsConfig.Bucket)
   385  		if err != nil {
   386  			return "", fmt.Errorf("calculating joburl: %w", err)
   387  		}
   388  
   389  		// Final path will be, e.g.:
   390  		// prefix.Scheme + prefix.Host + prefix.Path + storageProvider + bucketName         + gcsPath
   391  		// https://prow.k8s.io/view/                 + gs/             + kubernetes-jenkins + pr-logs/pull/kubernetes-sigs_cluster-api-provider-openstack/541/pull-cluster-api-provider-openstack-test/1247344427123347459
   392  		if plank.JobURLPrefixDisableAppendStorageProvider {
   393  			prefix.Path = path.Join(prefix.Path, prowPath.FullPath(), gcsPath)
   394  		} else {
   395  			prefix.Path = path.Join(prefix.Path, prowPath.StorageProvider(), prowPath.FullPath(), gcsPath)
   396  		}
   397  		return prefix.String(), nil
   398  	}
   399  	var b bytes.Buffer
   400  	if err := plank.JobURLTemplate.Execute(&b, &pj); err != nil {
   401  		log.WithFields(ProwJobFields(&pj)).Errorf("error executing URL template: %v", err)
   402  	} else {
   403  		return b.String(), nil
   404  	}
   405  	return "", nil
   406  }
   407  
   408  // ClusterToCtx converts the prow job's cluster to a cluster context
   409  func ClusterToCtx(cluster string) string {
   410  	if cluster == kube.InClusterContext {
   411  		return kube.DefaultClusterAlias
   412  	}
   413  	return cluster
   414  }
   415  
   416  func boolPtr(b bool) *bool {
   417  	return &b
   418  }