sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/crier/reporters/github/reporter.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package reporter implements a reporter interface for github
    18  // TODO(krzyzacy): move logic from report.go here
    19  package github
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/sirupsen/logrus"
    29  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    30  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    31  
    32  	v1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    33  	"sigs.k8s.io/prow/pkg/config"
    34  	"sigs.k8s.io/prow/pkg/crier/reporters/criercommonlib"
    35  	"sigs.k8s.io/prow/pkg/github/report"
    36  	"sigs.k8s.io/prow/pkg/kube"
    37  )
    38  
    39  const (
    40  	// GitHubReporterName is the name for github reporter
    41  	GitHubReporterName = "github-reporter"
    42  )
    43  
    44  // Client is a github reporter client
    45  type Client struct {
    46  	gc          report.GitHubClient
    47  	config      config.Getter
    48  	reportAgent v1.ProwJobAgent
    49  	prLocks     *criercommonlib.ShardedLock
    50  	lister      ctrlruntimeclient.Reader
    51  }
    52  
    53  // NewReporter returns a reporter client
    54  func NewReporter(gc report.GitHubClient, cfg config.Getter, reportAgent v1.ProwJobAgent, lister ctrlruntimeclient.Reader) *Client {
    55  	c := &Client{
    56  		gc:          gc,
    57  		config:      cfg,
    58  		reportAgent: reportAgent,
    59  		prLocks:     criercommonlib.NewShardedLock(),
    60  		lister:      lister,
    61  	}
    62  	c.prLocks.RunCleanup()
    63  	return c
    64  }
    65  
    66  // GetName returns the name of the reporter
    67  func (c *Client) GetName() string {
    68  	return GitHubReporterName
    69  }
    70  
    71  // ShouldReport returns if this prowjob should be reported by the github reporter
    72  func (c *Client) ShouldReport(_ context.Context, _ *logrus.Entry, pj *v1.ProwJob) bool {
    73  	if !pj.Spec.Report {
    74  		return false
    75  	}
    76  
    77  	switch {
    78  	case pj.Labels[kube.GerritReportLabel] != "":
    79  		return false // TODO(fejta): opt-in to github reporting
    80  	case pj.Spec.Type != v1.PresubmitJob && pj.Spec.Type != v1.PostsubmitJob:
    81  		return false // Report presubmit and postsubmit github jobs for github reporter
    82  	case c.reportAgent != "" && pj.Spec.Agent != c.reportAgent:
    83  		return false // Only report for specified agent
    84  	}
    85  
    86  	return true
    87  }
    88  
    89  // Report will report via reportlib
    90  func (c *Client) Report(ctx context.Context, log *logrus.Entry, pj *v1.ProwJob) ([]*v1.ProwJob, *reconcile.Result, error) {
    91  	ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
    92  	defer cancel()
    93  
    94  	// TODO(krzyzacy): ditch ReportTemplate, and we can drop reference to config.Getter
    95  	err := report.ReportStatusContext(ctx, c.gc, *pj, c.config().GitHubReporter)
    96  	if err != nil {
    97  		if strings.Contains(err.Error(), "This SHA and context has reached the maximum number of statuses") {
    98  			// This is completely unrecoverable, so just swallow the error to make sure we wont retry, even when crier gets restarted.
    99  			log.WithError(err).Debug("Encountered an error, skipping retries")
   100  			err = nil
   101  		} else if strings.Contains(err.Error(), "\"message\":\"Not Found\"") || strings.Contains(err.Error(), "\"message\":\"No commit found for SHA:") {
   102  			// "message":"Not Found" error occurs when someone force push, which is not a crier error
   103  			log.WithError(err).Debug("Could not find PR commit, skipping retries")
   104  			err = nil
   105  		}
   106  		// Always return when there is any error reporting status context.
   107  		return []*v1.ProwJob{pj}, nil, err
   108  	}
   109  
   110  	// The github comment create/update/delete done for presubmits
   111  	// needs pr-level locking to avoid racing when reporting multiple
   112  	// jobs in parallel.
   113  	if pj.Spec.Type == v1.PresubmitJob {
   114  		key, err := lockKeyForPJ(pj)
   115  		if err != nil {
   116  			return nil, nil, fmt.Errorf("failed to get lockkey for job: %w", err)
   117  		}
   118  		lock, err := c.prLocks.GetLock(ctx, *key)
   119  		if err != nil {
   120  			return nil, nil, err
   121  		}
   122  		if err := lock.Acquire(ctx, 1); err != nil {
   123  			return nil, nil, err
   124  		}
   125  		defer lock.Release(1)
   126  	}
   127  
   128  	// Check if this org or repo has opted out of failure report comments.
   129  	// This check has to be here and not in ShouldReport as we always need to report
   130  	// the status context, just potentially not creating a comment.
   131  	refs := pj.Spec.Refs
   132  	fullRepo := fmt.Sprintf("%s/%s", refs.Org, refs.Repo)
   133  	for _, ident := range c.config().GitHubReporter.NoCommentRepos {
   134  		if refs.Org == ident || fullRepo == ident {
   135  			return []*v1.ProwJob{pj}, nil, nil
   136  		}
   137  	}
   138  	// Check if this org or repo has opted out of failure report comments
   139  	toReport := []v1.ProwJob{*pj}
   140  	var mustCreateComment bool
   141  	for _, ident := range c.config().GitHubReporter.SummaryCommentRepos {
   142  		if pj.Spec.Refs.Org == ident || fullRepo == ident {
   143  			mustCreateComment = true
   144  			toReport, err = pjsToReport(ctx, log, c.lister, pj)
   145  			if err != nil {
   146  				return []*v1.ProwJob{pj}, nil, err
   147  			}
   148  		}
   149  	}
   150  	err = report.ReportComment(ctx, c.gc, c.config().Plank.ReportTemplateForRepo(pj.Spec.Refs), toReport, c.config().GitHubReporter, mustCreateComment)
   151  
   152  	return []*v1.ProwJob{pj}, nil, err
   153  }
   154  
   155  func pjsToReport(ctx context.Context, log *logrus.Entry, lister ctrlruntimeclient.Reader, pj *v1.ProwJob) ([]v1.ProwJob, error) {
   156  	if len(pj.Spec.Refs.Pulls) != 1 {
   157  		return nil, nil
   158  	}
   159  	// find all prowjobs from this PR
   160  	selector := map[string]string{}
   161  	for _, l := range []string{kube.OrgLabel, kube.RepoLabel, kube.PullLabel} {
   162  		selector[l] = pj.ObjectMeta.Labels[l]
   163  	}
   164  	var pjs v1.ProwJobList
   165  	if err := lister.List(ctx, &pjs, ctrlruntimeclient.MatchingLabels(selector)); err != nil {
   166  		return nil, fmt.Errorf("Cannot list prowjob with selector %v", selector)
   167  	}
   168  
   169  	latestBatch := make(map[string]v1.ProwJob)
   170  	for _, pjob := range pjs.Items {
   171  		if !pjob.Complete() { // Any job still running should prevent from comments
   172  			return nil, nil
   173  		}
   174  		if !pjob.Spec.Report { // Filtering out non-reporting jobs
   175  			continue
   176  		}
   177  		// Now you have convinced me that you are the same job from my revision,
   178  		// continue convince me that you are the last one of your kind
   179  		if existing, ok := latestBatch[pjob.Spec.Job]; !ok {
   180  			latestBatch[pjob.Spec.Job] = pjob
   181  		} else if pjob.CreationTimestamp.After(existing.CreationTimestamp.Time) {
   182  			latestBatch[pjob.Spec.Job] = pjob
   183  		}
   184  	}
   185  
   186  	var toReport []v1.ProwJob
   187  	for _, pjob := range latestBatch {
   188  		toReport = append(toReport, pjob)
   189  	}
   190  
   191  	return toReport, nil
   192  }
   193  
   194  func lockKeyForPJ(pj *v1.ProwJob) (*criercommonlib.SimplePull, error) {
   195  	if pj.Spec.Type != v1.PresubmitJob {
   196  		return nil, fmt.Errorf("can only get lock key for presubmit jobs, was %q", pj.Spec.Type)
   197  	}
   198  	if pj.Spec.Refs == nil {
   199  		return nil, errors.New("pj.Spec.Refs is nil")
   200  	}
   201  	if n := len(pj.Spec.Refs.Pulls); n != 1 {
   202  		return nil, fmt.Errorf("prowjob doesn't have one but %d pulls", n)
   203  	}
   204  	return criercommonlib.NewSimplePull(pj.Spec.Refs.Org, pj.Spec.Refs.Repo, pj.Spec.Refs.Pulls[0].Number), nil
   205  }