k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/testgrid/pkg/configurator/prow/prow.go (about)

     1  /*
     2  Copyright 2021 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 prow
    18  
    19  import (
    20  	"fmt"
    21  	"path"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/GoogleCloudPlatform/testgrid/config"
    27  	"github.com/GoogleCloudPlatform/testgrid/config/yamlcfg"
    28  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    29  
    30  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    31  	prowConfig "sigs.k8s.io/prow/pkg/config"
    32  	"sigs.k8s.io/prow/pkg/pjutil"
    33  	"sigs.k8s.io/prow/pkg/pod-utils/downwardapi"
    34  	prowGCS "sigs.k8s.io/prow/pkg/pod-utils/gcs"
    35  )
    36  
    37  const testgridCreateTestGroupAnnotation = "testgrid-create-test-group"
    38  const testgridDashboardsAnnotation = "testgrid-dashboards"
    39  const testgridTabNameAnnotation = "testgrid-tab-name"
    40  const testgridEmailAnnotation = "testgrid-alert-email"
    41  const testgridNumColumnsRecentAnnotation = "testgrid-num-columns-recent"
    42  const testgridAlertStaleResultsHoursAnnotation = "testgrid-alert-stale-results-hours"
    43  const testgridNumFailuresToAlertAnnotation = "testgrid-num-failures-to-alert"
    44  const testgridDaysOfResultsAnnotation = "testgrid-days-of-results"
    45  const testgridInCellMetric = "testgrid-in-cell-metric"
    46  const testGridDisableProwJobAnalysis = "testgrid-disable-prowjob-analysis"
    47  const testgridBaseOptionsAnnotation = "testgrid-base-options"
    48  const testgridBrokenColumnThreshold = "testgrid-broken-column-threshold"
    49  const descriptionAnnotation = "description"
    50  const minPresubmitNumColumnsRecent = 20
    51  
    52  // Talk to @michelle192837 if you're thinking about adding more of these!
    53  
    54  type ProwAwareConfigurator struct {
    55  	ProwConfig            *prowConfig.Config
    56  	DefaultTestgridConfig *yamlcfg.DefaultConfiguration
    57  
    58  	UpdateDescription bool
    59  	ProwJobConfigPath string
    60  	ProwJobURLPrefix  string
    61  }
    62  
    63  func (pac *ProwAwareConfigurator) TabDescriptionForProwJob(j prowConfig.JobBase) string {
    64  	fields := []string{}
    65  	fields = append(fields, fmt.Sprintf("prowjob_name: %v", j.Name))
    66  	if pac.ProwJobURLPrefix != "" {
    67  		url := pac.ProwJobURLPrefix + strings.TrimPrefix(j.SourcePath, pac.ProwJobConfigPath)
    68  		fields = append(fields, fmt.Sprintf("prowjob_config_url: %v", url))
    69  	}
    70  	if d := j.Annotations[descriptionAnnotation]; d != "" {
    71  		fields = append(fields, fmt.Sprintf("prowjob_description: %v", d))
    72  		if !pac.UpdateDescription {
    73  			return d
    74  		}
    75  	}
    76  	return strings.Join(fields, "\n")
    77  }
    78  
    79  func (pac *ProwAwareConfigurator) ApplySingleProwjobAnnotations(c *configpb.Configuration, j prowConfig.JobBase, pj prowapi.ProwJob) error {
    80  	tabName := j.Name
    81  	testGroupName := j.Name
    82  	var repo string
    83  	if pj.Spec.Refs != nil {
    84  		repo = fmt.Sprintf("%s/%s", pj.Spec.Refs.Org, pj.Spec.Refs.Repo)
    85  	}
    86  
    87  	pc := pac.ProwConfig
    88  	dc := pac.DefaultTestgridConfig
    89  
    90  	mustMakeGroup := j.Annotations[testgridCreateTestGroupAnnotation] == "true"
    91  	mustNotMakeGroup := j.Annotations[testgridCreateTestGroupAnnotation] == "false"
    92  	dashboards, addToDashboards := j.Annotations[testgridDashboardsAnnotation]
    93  	mightMakeGroup := (mustMakeGroup || addToDashboards || pj.Spec.Type != prowapi.PresubmitJob) && !mustNotMakeGroup
    94  	var testGroup *configpb.TestGroup
    95  
    96  	if mightMakeGroup {
    97  		if testGroup = config.FindTestGroup(testGroupName, c); testGroup != nil {
    98  			if mustMakeGroup {
    99  				return fmt.Errorf("test group %q already exists", testGroupName)
   100  			}
   101  		} else {
   102  			var prefix string
   103  			if j.DecorationConfig != nil && j.DecorationConfig.GCSConfiguration != nil {
   104  				prefix = path.Join(j.DecorationConfig.GCSConfiguration.Bucket, j.DecorationConfig.GCSConfiguration.PathPrefix)
   105  			} else if def := pc.Plank.GuessDefaultDecorationConfig(repo, ""); def != nil && def.GCSConfiguration != nil {
   106  				prefix = path.Join(def.GCSConfiguration.Bucket, def.GCSConfiguration.PathPrefix)
   107  			} else {
   108  				return fmt.Errorf("job %s: couldn't figure out a default decoration config", j.Name)
   109  			}
   110  
   111  			testGroup = &configpb.TestGroup{
   112  				Name:      testGroupName,
   113  				GcsPrefix: path.Join(prefix, prowGCS.RootForSpec(&downwardapi.JobSpec{Job: j.Name, Type: pj.Spec.Type})),
   114  			}
   115  			if dc != nil {
   116  				yamlcfg.ReconcileTestGroup(testGroup, dc.DefaultTestGroup)
   117  			}
   118  			c.TestGroups = append(c.TestGroups, testGroup)
   119  		}
   120  	} else {
   121  		testGroup = config.FindTestGroup(testGroupName, c)
   122  	}
   123  
   124  	if testGroup == nil {
   125  		for _, a := range []string{testgridNumColumnsRecentAnnotation, testgridAlertStaleResultsHoursAnnotation,
   126  			testgridNumFailuresToAlertAnnotation, testgridDaysOfResultsAnnotation, testgridTabNameAnnotation, testgridEmailAnnotation} {
   127  			_, ok := j.Annotations[a]
   128  			if ok {
   129  				return fmt.Errorf("no testgroup exists for job %q, but annotation %q implies one should exist", j.Name, a)
   130  			}
   131  		}
   132  		// exit early: with no test group, there's nothing else for us to usefully do with the job.
   133  		return nil
   134  	}
   135  
   136  	if ncr, ok := j.Annotations[testgridNumColumnsRecentAnnotation]; ok {
   137  		ncrInt, err := strconv.ParseInt(ncr, 10, 32)
   138  		if err != nil {
   139  			return fmt.Errorf("%s value %q is not a valid integer", testgridNumColumnsRecentAnnotation, ncr)
   140  		}
   141  		testGroup.NumColumnsRecent = int32(ncrInt)
   142  	} else if pj.Spec.Type == prowapi.PresubmitJob && testGroup.NumColumnsRecent < minPresubmitNumColumnsRecent {
   143  		testGroup.NumColumnsRecent = minPresubmitNumColumnsRecent
   144  	}
   145  
   146  	if dora, ok := j.Annotations[testgridDaysOfResultsAnnotation]; ok {
   147  		doraInt, err := strconv.ParseInt(dora, 10, 32)
   148  		if err != nil {
   149  			return fmt.Errorf("%s value %q is not a valid integer", testgridDaysOfResultsAnnotation, dora)
   150  		}
   151  		testGroup.DaysOfResults = int32(doraInt)
   152  	}
   153  
   154  	if stm, ok := j.Annotations[testgridInCellMetric]; ok {
   155  		testGroup.ShortTextMetric = stm
   156  	}
   157  
   158  	if dpa, ok := j.Annotations[testGridDisableProwJobAnalysis]; ok {
   159  		dpaBool, err := strconv.ParseBool(dpa)
   160  		if err != nil {
   161  			return fmt.Errorf("%s value %q in not a valid boolean", testGridDisableProwJobAnalysis, dpa)
   162  		}
   163  		testGroup.DisableProwjobAnalysis = dpaBool
   164  	}
   165  
   166  	if nfta, ok := j.Annotations[testgridNumFailuresToAlertAnnotation]; ok {
   167  		nftaInt, err := strconv.ParseInt(nfta, 10, 32)
   168  		if err != nil {
   169  			return fmt.Errorf("%s value %q is not a valid integer", testgridNumFailuresToAlertAnnotation, nfta)
   170  		}
   171  		testGroup.NumFailuresToAlert = int32(nftaInt) //nolint // The updater (and tabulator, when a feature flag is not set) still depend on the test group field.
   172  	}
   173  
   174  	if tn, ok := j.Annotations[testgridTabNameAnnotation]; ok {
   175  		tabName = tn
   176  	}
   177  
   178  	var baseOptions string
   179  	if bo, ok := j.Annotations[testgridBaseOptionsAnnotation]; ok {
   180  		baseOptions = bo
   181  	}
   182  
   183  	var brokenColumnThreshold float32
   184  	if bct, ok := j.Annotations[testgridBrokenColumnThreshold]; ok {
   185  		bctFloat, err := strconv.ParseFloat(bct, 32)
   186  		if err != nil {
   187  			return fmt.Errorf("%s value %q is not a valid float", testgridBrokenColumnThreshold, bct)
   188  		}
   189  		brokenColumnThreshold = float32(bctFloat)
   190  	}
   191  
   192  	description := pac.TabDescriptionForProwJob(j)
   193  
   194  	if addToDashboards {
   195  		firstDashboard := true
   196  		for _, dashboardName := range strings.Split(dashboards, ",") {
   197  			dashboardName = strings.TrimSpace(dashboardName)
   198  			d := config.FindDashboard(dashboardName, c)
   199  			if d == nil {
   200  				return fmt.Errorf("couldn't find dashboard %q for job %q", dashboardName, j.Name)
   201  			}
   202  			if repo == "" {
   203  				if len(j.ExtraRefs) > 0 {
   204  					repo = fmt.Sprintf("%s/%s", j.ExtraRefs[0].Org, j.ExtraRefs[0].Repo)
   205  				}
   206  			}
   207  			var codeSearchLinkTemplate, openBugLinkTemplate *configpb.LinkTemplate
   208  			if repo != "" {
   209  				codeSearchLinkTemplate = &configpb.LinkTemplate{
   210  					Url: fmt.Sprintf("https://github.com/%s/compare/<start-custom-0>...<end-custom-0>", repo),
   211  				}
   212  				openBugLinkTemplate = &configpb.LinkTemplate{
   213  					Url: fmt.Sprintf("https://github.com/%s/issues/", repo),
   214  				}
   215  			}
   216  
   217  			jobURLPrefix := pac.ProwConfig.Plank.GetJobURLPrefix(&pj)
   218  			var openTestLinkTemplate *configpb.LinkTemplate
   219  			if jobURLPrefix != "" {
   220  				openTestLinkTemplate = &configpb.LinkTemplate{
   221  					Url: strings.TrimRight(jobURLPrefix, "/") + "/gs/<gcs_prefix>/<changelist>",
   222  				}
   223  			}
   224  
   225  			dt := &configpb.DashboardTab{
   226  				Name:                  tabName,
   227  				TestGroupName:         testGroupName,
   228  				Description:           description,
   229  				CodeSearchUrlTemplate: codeSearchLinkTemplate,
   230  				OpenBugTemplate:       openBugLinkTemplate,
   231  				OpenTestTemplate:      openTestLinkTemplate,
   232  				BaseOptions:           baseOptions,
   233  				BrokenColumnThreshold: brokenColumnThreshold,
   234  			}
   235  			if firstDashboard {
   236  				firstDashboard = false
   237  				if emails, ok := j.Annotations[testgridEmailAnnotation]; ok {
   238  					initAlertOptions(dt)
   239  					dt.AlertOptions.AlertMailToAddresses = emails
   240  				}
   241  			}
   242  			if srh, ok := j.Annotations[testgridAlertStaleResultsHoursAnnotation]; ok {
   243  				srhInt, err := strconv.ParseInt(srh, 10, 32)
   244  				if err != nil {
   245  					return fmt.Errorf("%s value %q is not a valid integer", testgridAlertStaleResultsHoursAnnotation, srh)
   246  				}
   247  				initAlertOptions(dt)
   248  				dt.AlertOptions.AlertStaleResultsHours = int32(srhInt)
   249  			}
   250  			if nfta, ok := j.Annotations[testgridNumFailuresToAlertAnnotation]; ok {
   251  				nftaInt, err := strconv.ParseInt(nfta, 10, 32)
   252  				if err != nil {
   253  					return fmt.Errorf("%s value %q is not a valid integer", testgridNumFailuresToAlertAnnotation, nfta)
   254  				}
   255  				initAlertOptions(dt)
   256  				dt.AlertOptions.NumFailuresToAlert = int32(nftaInt)
   257  			}
   258  			if dc != nil {
   259  				yamlcfg.ReconcileDashboardTab(dt, dc.DefaultDashboardTab)
   260  			}
   261  			d.DashboardTab = append(d.DashboardTab, dt)
   262  		}
   263  	}
   264  
   265  	return nil
   266  }
   267  
   268  func initAlertOptions(dt *configpb.DashboardTab) {
   269  	if dt.AlertOptions == nil {
   270  		dt.AlertOptions = &configpb.DashboardTabAlertOptions{}
   271  	}
   272  }
   273  
   274  // sortPeriodics sorts all periodics by name (ascending).
   275  func sortPeriodics(per []prowConfig.Periodic) {
   276  	sort.Slice(per, func(a, b int) bool {
   277  		return per[a].Name < per[b].Name
   278  	})
   279  }
   280  
   281  // sortPostsubmits sorts all postsubmits by name and returns a sorted list of org/repos (ascending).
   282  func sortPostsubmits(post map[string][]prowConfig.Postsubmit) []string {
   283  	postRepos := make([]string, 0, len(post))
   284  
   285  	for k := range post {
   286  		postRepos = append(postRepos, k)
   287  	}
   288  
   289  	sort.Strings(postRepos)
   290  
   291  	for _, orgrepo := range postRepos {
   292  		sort.Slice(post[orgrepo], func(a, b int) bool {
   293  			return post[orgrepo][a].Name < post[orgrepo][b].Name
   294  		})
   295  	}
   296  
   297  	return postRepos
   298  }
   299  
   300  // sortPresubmits sorts all presubmits by name and returns a sorted list of org/repos (ascending).
   301  func sortPresubmits(pre map[string][]prowConfig.Presubmit) []string {
   302  	preRepos := make([]string, 0, len(pre))
   303  
   304  	for k := range pre {
   305  		preRepos = append(preRepos, k)
   306  	}
   307  
   308  	sort.Strings(preRepos)
   309  
   310  	for _, orgrepo := range preRepos {
   311  		sort.Slice(pre[orgrepo], func(a, b int) bool {
   312  			return pre[orgrepo][a].Name < pre[orgrepo][b].Name
   313  		})
   314  	}
   315  
   316  	return preRepos
   317  }
   318  
   319  func (pac *ProwAwareConfigurator) ApplyProwjobAnnotations(testgridConfig *configpb.Configuration) error {
   320  	if pac.ProwConfig == nil {
   321  		return nil
   322  	}
   323  	jobs := pac.ProwConfig.JobConfig
   324  
   325  	per := jobs.AllPeriodics()
   326  	sortPeriodics(per)
   327  	for _, j := range per {
   328  		pjSpec := pjutil.PeriodicSpec(prowConfig.Periodic{JobBase: j.JobBase})
   329  		pj := pjutil.NewProwJob(pjSpec, nil, nil)
   330  		if err := pac.ApplySingleProwjobAnnotations(testgridConfig, j.JobBase, pj); err != nil {
   331  			return err
   332  		}
   333  	}
   334  
   335  	post := jobs.PostsubmitsStatic
   336  	postReposSorted := sortPostsubmits(post)
   337  	for _, orgrepo := range postReposSorted {
   338  		items := strings.Split(orgrepo, "/")
   339  		for _, j := range post[orgrepo] {
   340  			pjSpec := pjutil.PostsubmitSpec(
   341  				prowConfig.Postsubmit{JobBase: j.JobBase},
   342  				prowapi.Refs{
   343  					Org:  items[0],
   344  					Repo: items[1],
   345  				},
   346  			)
   347  			pj := pjutil.NewProwJob(pjSpec, nil, nil)
   348  
   349  			if err := pac.ApplySingleProwjobAnnotations(testgridConfig, j.JobBase, pj); err != nil {
   350  				return err
   351  			}
   352  		}
   353  	}
   354  
   355  	pre := jobs.PresubmitsStatic
   356  	preReposSorted := sortPresubmits(pre)
   357  	for _, orgrepo := range preReposSorted {
   358  		items := strings.Split(orgrepo, "/")
   359  		for _, j := range pre[orgrepo] {
   360  			pjSpec := pjutil.PresubmitSpec(
   361  				prowConfig.Presubmit{JobBase: j.JobBase},
   362  				prowapi.Refs{
   363  					Org:  items[0],
   364  					Repo: items[1],
   365  				},
   366  			)
   367  			pj := pjutil.NewProwJob(pjSpec, nil, nil)
   368  			if err := pac.ApplySingleProwjobAnnotations(testgridConfig, j.JobBase, pj); err != nil {
   369  				return err
   370  			}
   371  		}
   372  	}
   373  
   374  	return nil
   375  }