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 }