github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/gangway/gangway.go (about) 1 /* 2 Copyright 2022 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 gangway 18 19 import ( 20 context "context" 21 "errors" 22 "fmt" 23 "regexp" 24 "strings" 25 26 "github.com/sirupsen/logrus" 27 codes "google.golang.org/grpc/codes" 28 "google.golang.org/grpc/metadata" 29 status "google.golang.org/grpc/status" 30 v1 "k8s.io/api/core/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/util/validation" 33 prowcrd "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 34 "sigs.k8s.io/prow/pkg/config" 35 "sigs.k8s.io/prow/pkg/kube" 36 "sigs.k8s.io/prow/pkg/pjutil" 37 "sigs.k8s.io/prow/pkg/version" 38 ) 39 40 const ( 41 HEADER_API_CONSUMER_TYPE = "x-endpoint-api-consumer-type" 42 HEADER_API_CONSUMER_ID = "x-endpoint-api-consumer-number" 43 ) 44 45 type Gangway struct { 46 UnimplementedProwServer 47 ConfigAgent *config.Agent 48 ProwJobClient ProwJobClient 49 InRepoConfigGetter config.InRepoConfigGetter 50 } 51 52 // ProwJobClient describes a Kubernetes client for the Prow Job CR. Unlike a 53 // general-purpose client, it only expects 2 methods, Create() and Get(). 54 type ProwJobClient interface { 55 Create(context.Context, *prowcrd.ProwJob, metav1.CreateOptions) (*prowcrd.ProwJob, error) 56 Get(context.Context, string, metav1.GetOptions) (*prowcrd.ProwJob, error) 57 } 58 59 // CreateJobExecution triggers a new Prow job. 60 func (gw *Gangway) CreateJobExecution(ctx context.Context, cjer *CreateJobExecutionRequest) (*JobExecution, error) { 61 err, md := getHttpRequestHeaders(ctx) 62 63 if err != nil { 64 logrus.WithError(err).Debug("could not find request HTTP headers") 65 return nil, status.Error(codes.InvalidArgument, err.Error()) 66 } 67 68 // Validate request fields. 69 if err := cjer.Validate(); err != nil { 70 logrus.WithError(err).Debug("could not validate request fields") 71 return nil, status.Error(codes.InvalidArgument, err.Error()) 72 } 73 74 // FIXME (listx) Add execution token generation database call, so that we can 75 // reduce the delay between the initial call and the creation of the ProwJob 76 // CR. We should probably use UUIDv7 (see 77 // https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html). 78 // Also see FireBase's PushID for comparison: 79 // https://firebase.blog/posts/2015/02/the-2120-ways-to-ensure-unique_68. 80 81 // Identify the client from the request metadata. 82 mainConfig := ProwCfgAdapter{gw.ConfigAgent.Config()} 83 allowedApiClient, err := mainConfig.IdentifyAllowedClient(md) 84 if err != nil { 85 logrus.WithError(err).Debug("could not find client in allowlist") 86 return nil, status.Error(codes.InvalidArgument, err.Error()) 87 } 88 89 l, err := getDecoratedLoggerEntry(allowedApiClient, md) 90 if err != nil { 91 l = logrus.NewEntry(logrus.New()) 92 } 93 94 allowedClusters := []string{"*"} 95 var reporterFunc ReporterFunc = nil 96 requireTenantID := true 97 98 jobExec, err := HandleProwJob(l, reporterFunc, cjer, gw.ProwJobClient, &mainConfig, gw.InRepoConfigGetter, allowedApiClient, requireTenantID, allowedClusters) 99 if err != nil { 100 logrus.WithError(err).Debugf("failed to create job %q", cjer.GetJobName()) 101 return nil, err 102 } 103 104 return jobExec, nil 105 } 106 107 // GetJobExecution returns a Prow job execution. It currently does this by 108 // looking at all of the existing Prow Job CR (custom resource) objects to find 109 // a match, and then does a translation from the CR into our JobExecution type. 110 // In the future this function will also perform a lookup in GCS or some other 111 // more permanent location as a fallback. 112 func (gw *Gangway) GetJobExecution(ctx context.Context, gjer *GetJobExecutionRequest) (*JobExecution, error) { 113 prowJobCR, err := gw.ProwJobClient.Get(context.TODO(), gjer.Id, metav1.GetOptions{}) 114 if err != nil { 115 return nil, err 116 } 117 118 var jobStatus JobExecutionStatus 119 120 // Translate ProwJobStatus.State in the Prow Job CR into a JobExecutionStatus. 121 switch prowJobCR.Status.State { 122 case prowcrd.TriggeredState: 123 jobStatus = JobExecutionStatus_TRIGGERED 124 case prowcrd.PendingState: 125 jobStatus = JobExecutionStatus_PENDING 126 case prowcrd.SuccessState: 127 jobStatus = JobExecutionStatus_SUCCESS 128 case prowcrd.FailureState: 129 jobStatus = JobExecutionStatus_FAILURE 130 case prowcrd.AbortedState: 131 jobStatus = JobExecutionStatus_ABORTED 132 case prowcrd.ErrorState: 133 jobStatus = JobExecutionStatus_ERROR 134 default: 135 jobStatus = JobExecutionStatus_JOB_EXECUTION_STATUS_UNSPECIFIED 136 137 } 138 139 jobExec := &JobExecution{ 140 Id: prowJobCR.Name, 141 JobStatus: jobStatus, 142 } 143 144 return jobExec, nil 145 } 146 147 // ClientAuthorized checks whether or not a client can run a Prow job based on 148 // the job's identifier (is this client allowed to run jobs meant for the given 149 // identifier?). This needs to traverse the config to determine whether the 150 // allowlist (allowed_api_clients) allows it. 151 func ClientAuthorized(allowedApiClient *config.AllowedApiClient, prowJobCR prowcrd.ProwJob) bool { 152 pjd := prowJobCR.Spec.ProwJobDefault 153 for _, allowedJobsFilter := range allowedApiClient.AllowedJobsFilters { 154 if allowedJobsFilter.TenantID == pjd.TenantID { 155 return true 156 } 157 } 158 return false 159 } 160 161 // FIXME: Add roundtrip tests to ensure that the conversion between gitRefs and 162 // refs is lossless. 163 func ToCrdRefs(gitRefs *Refs) (*prowcrd.Refs, error) { 164 if gitRefs == nil { 165 return nil, errors.New("gitRefs is nil") 166 } 167 168 refs := prowcrd.Refs{ 169 Org: gitRefs.Org, 170 Repo: gitRefs.Repo, 171 RepoLink: gitRefs.RepoLink, 172 BaseRef: gitRefs.BaseRef, 173 BaseSHA: gitRefs.BaseSha, 174 BaseLink: gitRefs.BaseLink, 175 PathAlias: gitRefs.PathAlias, 176 WorkDir: gitRefs.WorkDir, 177 CloneURI: gitRefs.CloneUri, 178 SkipSubmodules: gitRefs.SkipSubmodules, 179 CloneDepth: int(gitRefs.CloneDepth), 180 SkipFetchHead: gitRefs.SkipFetchHead, 181 } 182 183 var pulls []prowcrd.Pull 184 for _, pull := range gitRefs.GetPulls() { 185 if pull == nil { 186 continue 187 } 188 p := prowcrd.Pull{ 189 Number: int(pull.Number), 190 Author: pull.Author, 191 SHA: pull.Sha, 192 Title: pull.Title, 193 Ref: pull.Ref, 194 Link: pull.Link, 195 CommitLink: pull.CommitLink, 196 AuthorLink: pull.AuthorLink, 197 } 198 pulls = append(pulls, p) 199 } 200 201 refs.Pulls = pulls 202 203 return &refs, nil 204 } 205 206 func FromCrdRefs(refs *prowcrd.Refs) (*Refs, error) { 207 if refs == nil { 208 return nil, errors.New("refs is nil") 209 } 210 211 gitRefs := Refs{ 212 Org: refs.Org, 213 Repo: refs.Repo, 214 RepoLink: refs.RepoLink, 215 BaseRef: refs.BaseRef, 216 BaseSha: refs.BaseSHA, 217 BaseLink: refs.BaseLink, 218 PathAlias: refs.PathAlias, 219 WorkDir: refs.WorkDir, 220 CloneUri: refs.CloneURI, 221 SkipSubmodules: refs.SkipSubmodules, 222 CloneDepth: int32(refs.CloneDepth), 223 SkipFetchHead: refs.SkipFetchHead, 224 } 225 226 var pulls []*Pull 227 for _, pull := range refs.Pulls { 228 p := Pull{ 229 Number: int32(pull.Number), 230 Author: pull.Author, 231 Sha: pull.SHA, 232 Title: pull.Title, 233 Ref: pull.Ref, 234 Link: pull.Link, 235 CommitLink: pull.CommitLink, 236 AuthorLink: pull.AuthorLink, 237 } 238 pulls = append(pulls, &p) 239 } 240 241 gitRefs.Pulls = pulls 242 243 return &gitRefs, nil 244 } 245 246 func getHttpRequestHeaders(ctx context.Context) (error, *metadata.MD) { 247 // Retrieve HTTP headers from call. All headers are lower-cased. 248 md, ok := metadata.FromIncomingContext(ctx) 249 if !ok { 250 return fmt.Errorf("error retrieving metadata from context"), nil 251 } 252 return nil, &md 253 } 254 255 // getDecoratedLoggerEntry captures all known (interesting) HTTP headers of a 256 // gRPC request. We use these headers as log fields in the caller so that the 257 // logs can be very precise. 258 func getDecoratedLoggerEntry(allowedApiClient *config.AllowedApiClient, md *metadata.MD) (*logrus.Entry, error) { 259 cv, err := allowedApiClient.GetApiClientCloudVendor() 260 if err != nil { 261 return nil, err 262 } 263 264 knownHeaders := cv.GetRequiredMdHeaders() 265 fields := make(map[string]interface{}) 266 for _, header := range knownHeaders { 267 values := md.Get(header) 268 // Only use the first value. MD stores multiple values in case other 269 // entities attempt to overwrite an existing key (it prevents this by 270 // storing values as a list of strings). 271 // 272 // Prefix the field with "http-header/" so that all of the headers here 273 // get displayed neatly together (when the fields are sorted by logrus's 274 // own output to the console). 275 if len(values) > 0 { 276 fields[fmt.Sprintf("http-header/%s", header)] = values[0] 277 } 278 } 279 fields["component"] = version.Name 280 281 l := logrus.WithFields(fields) 282 283 return l, nil 284 } 285 286 func (cjer *CreateJobExecutionRequest) Validate() error { 287 jobName := cjer.GetJobName() 288 jobExecutionType := cjer.GetJobExecutionType() 289 gitRefs := cjer.GetRefs() 290 291 if len(jobName) == 0 { 292 return errors.New("job_name field cannot be empty") 293 } 294 295 if jobExecutionType == JobExecutionType_JOB_EXECUTION_TYPE_UNSPECIFIED { 296 return fmt.Errorf("unsupported JobExecutionType: %s", jobExecutionType) 297 } 298 299 // Periodic jobs are not allowed to be defined with gitRefs. This is because 300 // gitRefs can denote inrepoconfig repo information (and periodic jobs are 301 // not allowed to be defined via inrepoconfig). See 302 // https://github.com/kubernetes/test-infra/issues/21729. 303 if jobExecutionType == JobExecutionType_PERIODIC && gitRefs != nil { 304 logrus.Debug("periodic jobs cannot also have gitRefs") 305 return errors.New("periodic jobs cannot also have gitRefs") 306 } 307 308 if jobExecutionType != JobExecutionType_PERIODIC { 309 // Non-periodic jobs must have a BaseRepo (default repo to clone) 310 // defined. 311 if gitRefs == nil { 312 return fmt.Errorf("gitRefs must be defined for %q", jobExecutionType) 313 } 314 if err := gitRefs.Validate(); err != nil { 315 return fmt.Errorf("gitRefs: failed to validate: %s", err) 316 } 317 } 318 319 // Finally perform some additional checks on the requested PodSpecOptions. 320 podSpecOptions := cjer.GetPodSpecOptions() 321 if podSpecOptions != nil { 322 envs := podSpecOptions.GetEnvs() 323 for k, v := range envs { 324 if len(k) == 0 || len(v) == 0 { 325 return fmt.Errorf("invalid environment variable key/value pair: %q, %q", k, v) 326 } 327 } 328 329 labels := podSpecOptions.GetLabels() 330 for k, v := range labels { 331 if len(k) == 0 || len(v) == 0 { 332 return fmt.Errorf("invalid label key/value pair: %q, %q", k, v) 333 } 334 335 errs := validation.IsValidLabelValue(v) 336 if len(errs) > 0 { 337 return fmt.Errorf("invalid label: the following errors found: %q", errs) 338 } 339 } 340 341 annotations := podSpecOptions.GetAnnotations() 342 for k, v := range annotations { 343 if len(k) == 0 || len(v) == 0 { 344 return fmt.Errorf("invalid annotation key/value pair: %q, %q", k, v) 345 } 346 } 347 } 348 349 return nil 350 } 351 352 func (gitRefs *Refs) Validate() error { 353 if len(gitRefs.Org) == 0 { 354 return fmt.Errorf("gitRefs: Org cannot be empty") 355 } 356 357 if len(gitRefs.Repo) == 0 { 358 return fmt.Errorf("gitRefs: Repo cannot be empty") 359 } 360 361 if len(gitRefs.BaseRef) == 0 { 362 return fmt.Errorf("gitRefs: BaseRef cannot be empty") 363 } 364 365 if len(gitRefs.BaseSha) == 0 { 366 return fmt.Errorf("gitRefs: BaseSha cannot be empty") 367 } 368 369 for _, pull := range gitRefs.Pulls { 370 if err := pull.Validate(); err != nil { 371 return err 372 } 373 } 374 375 return nil 376 } 377 378 func (pull *Pull) Validate() error { 379 // Commit SHA must be a 40-character hex string. 380 var validSha = regexp.MustCompile(`^[0-9a-f]{40}$`) 381 if !validSha.MatchString(pull.Sha) { 382 return fmt.Errorf("pull: invalid SHA: %q", pull.Sha) 383 } 384 return nil 385 } 386 387 // Ensure interface is intact. I.e., this declaration ensures that the type 388 // "*config.Config" implements the "prowCfgClient" interface. See 389 // https://golang.org/doc/faq#guarantee_satisfies_interface. 390 var _ prowCfgClient = (*ProwCfgAdapter)(nil) 391 392 // prowCfgClient is a subset of all the various behaviors that the 393 // "*config.Config" type implements, which we will test here. 394 type prowCfgClient interface { 395 AllPeriodics() []config.Periodic 396 GetPresubmitsStatic(identifier string) []config.Presubmit 397 GetPostsubmitsStatic(identifier string) []config.Postsubmit 398 GetProwJobDefault(repo, cluster string) *prowcrd.ProwJobDefault 399 GetScheduler() config.Scheduler 400 } 401 402 type ProwCfgAdapter struct { 403 *config.Config 404 } 405 406 func (c *ProwCfgAdapter) GetScheduler() config.Scheduler { return c.Scheduler } 407 408 type ReporterFunc func(pj *prowcrd.ProwJob, state prowcrd.ProwJobState, err error) 409 410 func (cjer *CreateJobExecutionRequest) getJobHandler() (jobHandler, error) { 411 var jh jobHandler 412 switch cjer.GetJobExecutionType() { 413 case JobExecutionType_PERIODIC: 414 jh = &periodicJobHandler{} 415 case JobExecutionType_PRESUBMIT: 416 jh = &presubmitJobHandler{} 417 case JobExecutionType_POSTSUBMIT: 418 jh = &postsubmitJobHandler{} 419 default: 420 return nil, fmt.Errorf("unsupported JobExecutionType type: %s", cjer.GetJobExecutionType()) 421 } 422 423 return jh, nil 424 } 425 426 // Deep-copy all map fields from a gangway.CreateJobExecutionRequest and also 427 // the statically defined (configured in YAML) Prow Job labels and annotations. 428 func mergeMapFields(cjer *CreateJobExecutionRequest, staticLabels, staticAnnotations map[string]string) (map[string]string, map[string]string) { 429 430 pso := cjer.GetPodSpecOptions() 431 432 combinedLabels := make(map[string]string) 433 combinedAnnotations := make(map[string]string) 434 435 // Overwrite the static definitions with what we received in the 436 // CreateJobExecutionRequest. This order is important. 437 for k, v := range staticLabels { 438 combinedLabels[k] = v 439 } 440 for k, v := range pso.GetLabels() { 441 combinedLabels[k] = v 442 } 443 444 // Do the same for the annotations. 445 for k, v := range staticAnnotations { 446 combinedAnnotations[k] = v 447 } 448 for k, v := range pso.GetAnnotations() { 449 combinedAnnotations[k] = v 450 } 451 452 return combinedLabels, combinedAnnotations 453 } 454 455 func HandleProwJob(l *logrus.Entry, 456 reporterFunc ReporterFunc, 457 cjer *CreateJobExecutionRequest, 458 pjc ProwJobClient, 459 mainConfig prowCfgClient, 460 ircg config.InRepoConfigGetter, 461 allowedApiClient *config.AllowedApiClient, 462 requireTenantID bool, 463 allowedClusters []string) (*JobExecution, error) { 464 465 var prowJobCR prowcrd.ProwJob 466 467 var prowJobSpec *prowcrd.ProwJobSpec 468 var jh jobHandler 469 jh, err := cjer.getJobHandler() 470 if err != nil { 471 return nil, err 472 } 473 prowJobSpec, labels, annotations, err := jh.getProwJobSpec(mainConfig, ircg, cjer) 474 if err != nil { 475 // These are user errors, i.e. missing fields, requested prowjob doesn't exist etc. 476 // These errors are already surfaced to user via pubsub two lines below. 477 l.WithError(err).WithField("name", cjer.GetJobName()).Info("Failed getting prowjob spec") 478 prowJobCR = pjutil.NewProwJob(prowcrd.ProwJobSpec{}, nil, cjer.GetPodSpecOptions().GetAnnotations(), 479 pjutil.RequireScheduling(mainConfig.GetScheduler().Enabled)) 480 481 if reporterFunc != nil { 482 reporterFunc(&prowJobCR, prowcrd.ErrorState, err) 483 } 484 return nil, err 485 } 486 if prowJobSpec == nil { 487 return nil, fmt.Errorf("failed getting prowjob spec") // This should not happen 488 } 489 490 combinedLabels, combinedAnnotations := mergeMapFields(cjer, labels, annotations) 491 prowJobCR = pjutil.NewProwJob(*prowJobSpec, combinedLabels, combinedAnnotations, 492 pjutil.RequireScheduling(mainConfig.GetScheduler().Enabled)) 493 // Adds / Updates Environments to containers 494 if prowJobCR.Spec.PodSpec != nil { 495 for i, c := range prowJobCR.Spec.PodSpec.Containers { 496 for k, v := range cjer.GetPodSpecOptions().GetEnvs() { 497 c.Env = append(c.Env, v1.EnvVar{Name: k, Value: v}) 498 } 499 prowJobCR.Spec.PodSpec.Containers[i].Env = c.Env 500 } 501 } 502 503 // deny job that runs on not allowed cluster 504 var clusterIsAllowed bool 505 for _, allowedCluster := range allowedClusters { 506 if allowedCluster == "*" || allowedCluster == prowJobSpec.Cluster { 507 clusterIsAllowed = true 508 break 509 } 510 } 511 // This is a user error, not sure whether we want to return error here. 512 if !clusterIsAllowed { 513 err := fmt.Errorf("cluster %s is not allowed. Can be fixed by defining this cluster under pubsub_triggers -> allowed_clusters", prowJobSpec.Cluster) 514 l.WithField("cluster", prowJobSpec.Cluster).Warn("cluster not allowed") 515 if reporterFunc != nil { 516 reporterFunc(&prowJobCR, prowcrd.ErrorState, err) 517 } 518 return nil, err 519 } 520 521 // Figure out the tenantID defined for this job by looking it up in its 522 // config, or if that's missing, finding the default one specified in the 523 // main Config. 524 if requireTenantID { 525 var jobTenantID string 526 if prowJobCR.Spec.ProwJobDefault != nil && prowJobCR.Spec.ProwJobDefault.TenantID != "" { 527 jobTenantID = prowJobCR.Spec.ProwJobDefault.TenantID 528 } else { 529 // Derive the orgRepo from the request. Postsubmits and Presubmits both 530 // require Git refs information, so we can use that to get the job's 531 // associated orgRepo. Then we can feed this orgRepo into 532 // mainConfig.GetProwJobDefault(orgRepo, '*') to get the tenantID from 533 // the main Config's "prowjob_default_entries" field. 534 switch cjer.GetJobExecutionType() { 535 case JobExecutionType_POSTSUBMIT: 536 fallthrough 537 case JobExecutionType_PRESUBMIT: 538 orgRepo := fmt.Sprintf("%s/%s", cjer.GetRefs().GetOrg(), cjer.GetRefs().GetRepo()) 539 jobTenantID = mainConfig.GetProwJobDefault(orgRepo, "*").TenantID 540 } 541 } 542 543 if len(jobTenantID) == 0 { 544 return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("could not determine tenant_id for job %s", prowJobCR.Name)) 545 } 546 if prowJobCR.Spec.ProwJobDefault != nil { 547 prowJobCR.Spec.ProwJobDefault.TenantID = jobTenantID 548 } 549 } 550 551 // Check whether this authenticated API client has authorization to trigger 552 // the requested Prow Job. 553 if allowedApiClient != nil { 554 authorized := ClientAuthorized(allowedApiClient, prowJobCR) 555 556 if !authorized { 557 logrus.Error("client is not authorized to execute the given job") 558 return nil, status.Error(codes.PermissionDenied, "client is not authorized to execute the given job") 559 } 560 } 561 562 if _, err := pjc.Create(context.TODO(), &prowJobCR, metav1.CreateOptions{}); err != nil { 563 l.WithError(err).Errorf("failed to create job %q as %q", cjer.GetJobName(), prowJobCR.Name) 564 if reporterFunc != nil { 565 reporterFunc(&prowJobCR, prowcrd.ErrorState, err) 566 } 567 return nil, err 568 } 569 l.WithFields(logrus.Fields{ 570 "job": cjer.GetJobName(), 571 "name": prowJobCR.Name, 572 "prowjob-annotations": prowJobCR.Annotations, 573 }).Info("Job created.") 574 if reporterFunc != nil { 575 reporterFunc(&prowJobCR, prowcrd.TriggeredState, nil) 576 } 577 578 // Now populate a JobExecution. We have to convert data from the ProwJob 579 // custom resource to a JobExecution. For now we just reuse the "Name" field 580 // of a ProwJob CR as a globally-unique execution ID, because this existing 581 // string is already used to do lookups on Deck 582 // (https://prow.k8s.io/prowjob?prowjob=c2891365-621c-11ed-88b0-da2d50b4915c) 583 // but also for naming the test pod itself (prowcrd.ProwJob.Status.pod_name 584 // field). 585 jobExec := &JobExecution{ 586 Id: prowJobCR.Name, 587 JobName: cjer.GetJobName(), 588 JobType: cjer.GetJobExecutionType(), 589 JobStatus: JobExecutionStatus_TRIGGERED, 590 Refs: cjer.GetRefs(), 591 PodSpecOptions: cjer.GetPodSpecOptions(), 592 } 593 594 return jobExec, nil 595 } 596 597 // jobHandler handles job type specific logic 598 type jobHandler interface { 599 getProwJobSpec(mainConfig prowCfgClient, ircg config.InRepoConfigGetter, cjer *CreateJobExecutionRequest) (prowJobSpec *prowcrd.ProwJobSpec, labels map[string]string, annotations map[string]string, err error) 600 } 601 602 // periodicJobHandler implements jobHandler 603 type periodicJobHandler struct{} 604 605 func (peh *periodicJobHandler) getProwJobSpec(mainConfig prowCfgClient, ircg config.InRepoConfigGetter, cjer *CreateJobExecutionRequest) (prowJobSpec *prowcrd.ProwJobSpec, labels map[string]string, annotations map[string]string, err error) { 606 var periodicJob *config.Periodic 607 // TODO(chaodaiG): do we want to support inrepoconfig when 608 // https://github.com/kubernetes/test-infra/issues/21729 is done? 609 for _, job := range mainConfig.AllPeriodics() { 610 if job.Name == cjer.GetJobName() { 611 // Directly followed by break, so this is ok 612 // nolint: exportloopref 613 periodicJob = &job 614 break 615 } 616 } 617 if periodicJob == nil { 618 err = fmt.Errorf("failed to find associated periodic job %q", cjer.GetJobName()) 619 return 620 } 621 622 spec := pjutil.PeriodicSpec(*periodicJob) 623 prowJobSpec = &spec 624 labels, annotations = periodicJob.Labels, periodicJob.Annotations 625 return 626 } 627 628 // presubmitJobHandler implements jobHandler 629 type presubmitJobHandler struct { 630 } 631 632 // validateRefs performs some basic checks for the associated Refs provided with 633 // a Prow Job. This function is only meant to be used with the presubmit and 634 // postsubmit types. 635 func validateRefs(jobType JobExecutionType, refs *prowcrd.Refs) error { 636 637 switch jobType { 638 case JobExecutionType_PRESUBMIT: 639 break 640 case JobExecutionType_POSTSUBMIT: 641 break 642 default: 643 return fmt.Errorf("programmer error: validateRefs was used incorrectly for %q", jobType.String()) 644 } 645 646 if refs == nil { 647 return errors.New("Refs must be supplied") 648 } 649 if len(refs.Org) == 0 { 650 return errors.New("org must be supplied") 651 } 652 if len(refs.Repo) == 0 { 653 return errors.New("repo must be supplied") 654 } 655 if len(refs.BaseSHA) == 0 { 656 return errors.New("baseSHA must be supplied") 657 } 658 if len(refs.BaseRef) == 0 { 659 return errors.New("baseRef must be supplied") 660 } 661 if jobType == JobExecutionType_PRESUBMIT && len(refs.Pulls) == 0 { 662 return errors.New("at least 1 Pulls is required") 663 } 664 return nil 665 } 666 667 func (prh *presubmitJobHandler) getProwJobSpec(mainConfig prowCfgClient, ircg config.InRepoConfigGetter, cjer *CreateJobExecutionRequest) (prowJobSpec *prowcrd.ProwJobSpec, labels map[string]string, annotations map[string]string, err error) { 668 // presubmit jobs require Refs and Refs.Pulls to be set 669 refs, err := ToCrdRefs(cjer.GetRefs()) 670 if err != nil { 671 return 672 } 673 if err = validateRefs(cjer.GetJobExecutionType(), refs); err != nil { 674 return 675 } 676 677 var presubmitJob *config.Presubmit 678 org, repo, branch := refs.Org, refs.Repo, refs.BaseRef 679 orgRepo := org + "/" + repo 680 baseSHAGetter := func() (string, error) { 681 return refs.BaseSHA, nil 682 } 683 var headSHAGetters []func() (string, error) 684 for _, pull := range refs.Pulls { 685 pull := pull 686 headSHAGetters = append(headSHAGetters, func() (string, error) { 687 return pull.SHA, nil 688 }) 689 } 690 691 logger := logrus.WithFields(logrus.Fields{"org": org, "repo": repo, "branch": branch, "orgRepo": orgRepo}) 692 // Get presubmits from Config alone. 693 presubmits := mainConfig.GetPresubmitsStatic(orgRepo) 694 // If InRepoConfigGetter is provided, then it means that we also want to fetch 695 // from an inrepoconfig. 696 if ircg != nil { 697 logger.Debug("Getting prow jobs.") 698 var presubmitsWithInrepoconfig []config.Presubmit 699 var err error 700 prowYAML, err := ircg.GetInRepoConfig(orgRepo, branch, baseSHAGetter, headSHAGetters...) 701 if err != nil { 702 logger.WithError(err).Info("Failed to get presubmits") 703 } else { 704 logger.WithField("static-jobs", len(presubmits)).WithField("jobs-with-inrepoconfig", len(presubmitsWithInrepoconfig)).Debug("Jobs found.") 705 presubmits = append(presubmits, prowYAML.Presubmits...) 706 } 707 } 708 709 for _, job := range presubmits { 710 job := job 711 if !job.CouldRun(branch) { // filter out jobs that are not branch matching 712 continue 713 } 714 if job.Name == cjer.GetJobName() { 715 if presubmitJob != nil { 716 err = fmt.Errorf("%s matches multiple prow jobs from orgRepo %q", cjer.GetJobName(), orgRepo) 717 return 718 } 719 presubmitJob = &job 720 } 721 } 722 // This also captures the case where fetching jobs from inrepoconfig failed. 723 // However doesn't not distinguish between this case and a wrong prow job name. 724 if presubmitJob == nil { 725 err = fmt.Errorf("failed to find associated presubmit job %q from orgRepo %q", cjer.GetJobName(), orgRepo) 726 return 727 } 728 729 spec := pjutil.PresubmitSpec(*presubmitJob, *refs) 730 prowJobSpec, labels, annotations = &spec, presubmitJob.Labels, presubmitJob.Annotations 731 return 732 } 733 734 // postsubmitJobHandler implements jobHandler 735 type postsubmitJobHandler struct { 736 } 737 738 func (poh *postsubmitJobHandler) getProwJobSpec(mainConfig prowCfgClient, ircg config.InRepoConfigGetter, cjer *CreateJobExecutionRequest) (prowJobSpec *prowcrd.ProwJobSpec, labels map[string]string, annotations map[string]string, err error) { 739 // postsubmit jobs require Refs to be set 740 refs, err := ToCrdRefs(cjer.GetRefs()) 741 if err != nil { 742 return 743 } 744 if err = validateRefs(cjer.GetJobExecutionType(), refs); err != nil { 745 return 746 } 747 748 var postsubmitJob *config.Postsubmit 749 org, repo, branch := refs.Org, refs.Repo, refs.BaseRef 750 orgRepo := org + "/" + repo 751 // Add "https://" prefix to orgRepo if this is a gerrit job. 752 // (Unfortunately gerrit jobs use the full repo URL as the identifier.) 753 prefix := "https://" 754 psoLabels := cjer.GetPodSpecOptions().GetLabels() 755 if psoLabels != nil && psoLabels[kube.GerritRevision] != "" && !strings.HasPrefix(orgRepo, prefix) { 756 orgRepo = prefix + orgRepo 757 } 758 baseSHAGetter := func() (string, error) { 759 return refs.BaseSHA, nil 760 } 761 762 logger := logrus.WithFields(logrus.Fields{"org": org, "repo": repo, "branch": branch, "orgRepo": orgRepo}) 763 postsubmits := mainConfig.GetPostsubmitsStatic(orgRepo) 764 if ircg != nil { 765 logger.Debug("Getting prow jobs.") 766 var postsubmitsWithInrepoconfig []config.Postsubmit 767 var err error 768 prowYAML, err := ircg.GetInRepoConfig(orgRepo, branch, baseSHAGetter) 769 if err != nil { 770 logger.WithError(err).Info("Failed to get postsubmits from inrepoconfig") 771 } else { 772 logger.WithField("static-jobs", len(postsubmits)).WithField("jobs-with-inrepoconfig", len(postsubmitsWithInrepoconfig)).Debug("Jobs found.") 773 postsubmits = append(postsubmits, prowYAML.Postsubmits...) 774 } 775 } 776 777 for _, job := range postsubmits { 778 job := job 779 if !job.CouldRun(branch) { // filter out jobs that are not branch matching 780 continue 781 } 782 if job.Name == cjer.GetJobName() { 783 if postsubmitJob != nil { 784 return nil, nil, nil, fmt.Errorf("%s matches multiple prow jobs from orgRepo %q", cjer.GetJobName(), orgRepo) 785 } 786 postsubmitJob = &job 787 } 788 } 789 // This also captures the case where fetching jobs from inrepoconfig failed. 790 // However doesn't not distinguish between this case and a wrong prow job name. 791 if postsubmitJob == nil { 792 err = fmt.Errorf("failed to find associated postsubmit job %q from orgRepo %q", cjer.GetJobName(), orgRepo) 793 return 794 } 795 796 spec := pjutil.PostsubmitSpec(*postsubmitJob, *refs) 797 prowJobSpec, labels, annotations = &spec, postsubmitJob.Labels, postsubmitJob.Annotations 798 return 799 }