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 }