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  }