k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/config/tests/testgrids/config_test.go (about) 1 /* 2 Copyright 2016 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 testgrids 18 19 import ( 20 "context" 21 "flag" 22 "fmt" 23 "net/mail" 24 "os" 25 "path" 26 "path/filepath" 27 "regexp" 28 "strings" 29 "testing" 30 31 "k8s.io/apimachinery/pkg/util/sets" 32 33 "github.com/GoogleCloudPlatform/testgrid/config" 34 config_pb "github.com/GoogleCloudPlatform/testgrid/pb/config" 35 "k8s.io/test-infra/testgrid/pkg/configurator/configurator" 36 "k8s.io/test-infra/testgrid/pkg/configurator/options" 37 prow_config "sigs.k8s.io/prow/pkg/config" 38 "sigs.k8s.io/prow/pkg/flagutil" 39 40 configflagutil "sigs.k8s.io/prow/pkg/flagutil/config" 41 ) 42 43 type SQConfig struct { 44 Data map[string]string `yaml:"data,omitempty"` 45 } 46 47 var ( 48 companies = []string{ 49 "amazon", 50 "canonical", 51 "cos", 52 "containerd", 53 "cri-o", 54 "istio", 55 "googleoss", 56 "google", 57 "kopeio", 58 "redhat", 59 "ibm", 60 "vmware", 61 "gardener", 62 "jetstack", 63 "kubevirt", 64 } 65 orgs = []string{ 66 "conformance", 67 "kops", 68 "presubmits", 69 "sig", 70 "wg", 71 "provider", 72 "kubernetes-clients", 73 "kcp", 74 } 75 dashboardPrefixes = [][]string{orgs, companies} 76 77 // gcs prefixes populated by the kubernetes prow instance 78 prowGcsPrefixes = []string{ 79 "kubernetes-jenkins/logs/", 80 "kubernetes-jenkins/pr-logs/directory/", 81 } 82 ) 83 84 var defaultInputs options.MultiString = []string{"../../testgrids"} 85 var prowPath = flag.String("prow-config", "../../../config/prow/config.yaml", "Path to prow config") 86 var jobPath = flag.String("job-config", "../../jobs", "Path to prow job config") 87 var defaultYAML = flag.String("default", "../../testgrids/default.yaml", "Default yaml for testgrid") 88 var inputs options.MultiString 89 var protoPath = flag.String("config", "", "Path to TestGrid config proto") 90 91 // Shared testgrid config, loaded at TestMain. 92 var cfg *config_pb.Configuration 93 94 // Shared prow config, loaded at Test Main 95 var prowConfig *prow_config.Config 96 97 func TestMain(m *testing.M) { 98 flag.Var(&inputs, "yaml", "comma-separated list of input YAML files or directories") 99 flag.Parse() 100 if *protoPath == "" { 101 if len(inputs) == 0 { 102 inputs = defaultInputs 103 } 104 // Generate proto from testgrid config 105 tmpDir, err := os.MkdirTemp("", "testgrid-config-test") 106 if err != nil { 107 fmt.Println(err) 108 os.Exit(1) 109 } 110 defer os.RemoveAll(tmpDir) 111 tmpFile := path.Join(tmpDir, "test-proto") 112 113 opt := options.Options{ 114 Inputs: inputs, 115 ProwConfig: configflagutil.ConfigOptions{ 116 ConfigPath: *prowPath, 117 JobConfigPath: *jobPath, 118 }, 119 DefaultYAML: *defaultYAML, 120 Output: flagutil.NewStringsBeenSet(tmpFile), 121 Oneshot: true, 122 StrictUnmarshal: true, 123 } 124 125 if err := configurator.RealMain(&opt); err != nil { 126 fmt.Println(err) 127 os.Exit(1) 128 } 129 130 protoPath = &tmpFile 131 } 132 133 var err error 134 cfg, err = config.Read(context.Background(), *protoPath, nil) 135 if err != nil { 136 fmt.Printf("Could not load config: %v\n", err) 137 os.Exit(1) 138 } 139 140 prowConfig, err = prow_config.Load(*prowPath, *jobPath, nil, "") 141 if err != nil { 142 fmt.Printf("Could not load prow configs: %v\n", err) 143 os.Exit(1) 144 } 145 146 os.Exit(m.Run()) 147 } 148 149 func TestConfig(t *testing.T) { 150 // testgroup - occurrence map, validate testgroups 151 testgroupMap := make(map[string]int32) 152 153 for testgroupidx, testgroup := range cfg.TestGroups { 154 // All testgroup must have a name and a query 155 if testgroup.Name == "" || testgroup.GcsPrefix == "" { 156 t.Errorf("Testgroup #%v (Name: '%v', GcsPrefix: '%v'): - Must have a name and gcs_prefix", 157 testgroupidx, testgroup.Name, testgroup.GcsPrefix) 158 } 159 160 // All testgroup must not have duplicated names 161 if testgroupMap[testgroup.Name] > 0 { 162 t.Errorf("Duplicated Testgroup: %v", testgroup.Name) 163 } else { 164 testgroupMap[testgroup.Name] = 1 165 } 166 167 t.Run("Testgroup "+testgroup.Name, func(t *testing.T) { 168 if !testgroup.IsExternal { 169 t.Error("IsExternal must be true") 170 } 171 172 for hIdx, header := range testgroup.ColumnHeader { 173 if header.ConfigurationValue == "" { 174 t.Errorf("Column Header %d is empty", hIdx) 175 } 176 } 177 178 for _, prowGcsPrefix := range prowGcsPrefixes { 179 if strings.Contains(testgroup.GcsPrefix, prowGcsPrefix) { 180 // The expectation is that testgroup.Name is the name of a Prow job and the GCSPrefix 181 // follows the convention kubernetes-jenkins/logs/.../jobName 182 // The final part of the prefix should be the job name. 183 expected := filepath.Join(filepath.Dir(testgroup.GcsPrefix), testgroup.Name) 184 if expected != testgroup.GcsPrefix { 185 t.Errorf("GcsPrefix: Got %s; Want %s", testgroup.GcsPrefix, expected) 186 } 187 break // out of prowGcsPrefix for loop 188 } 189 } 190 191 if testgroup.TestNameConfig != nil { 192 if testgroup.TestNameConfig.NameFormat == "" { 193 t.Error("Empty NameFormat") 194 } 195 196 if got, want := len(testgroup.TestNameConfig.NameElements), strings.Count(testgroup.TestNameConfig.NameFormat, "%"); got != want { 197 t.Errorf("TestNameConfig has %d elements, format %s wants %d", got, testgroup.TestNameConfig.NameFormat, want) 198 } 199 } 200 201 // All PR testgroup has num_columns_recent equals 20 202 if strings.HasPrefix(testgroup.GcsPrefix, "kubernetes-jenkins/pr-logs/directory/") { 203 if testgroup.NumColumnsRecent < 20 { 204 t.Errorf("presubmit num_columns_recent want >=20, got %d", testgroup.NumColumnsRecent) 205 } 206 } 207 }) 208 } 209 210 // dashboard name set 211 dashboardSet := sets.NewString() 212 213 for dashboardidx, dashboard := range cfg.Dashboards { 214 // All dashboard must have a name 215 if dashboard.Name == "" { 216 t.Errorf("Dashboard %v: - Must have a name", dashboardidx) 217 } 218 219 found := false 220 for _, kind := range dashboardPrefixes { 221 for _, prefix := range kind { 222 if strings.HasPrefix(dashboard.Name, prefix) || dashboard.Name == prefix { 223 found = true 224 break 225 } 226 } 227 if found { 228 break 229 } 230 } 231 if !found { 232 t.Errorf("Dashboard %v: must prefix with one of: %v", dashboard.Name, dashboardPrefixes) 233 } 234 235 // All dashboard must not have duplicated names 236 if dashboardSet.Has(dashboard.Name) { 237 t.Errorf("Duplicated dashboard: %v", dashboard.Name) 238 } else { 239 dashboardSet.Insert(dashboard.Name) 240 } 241 242 // All dashboard must have at least one tab 243 if len(dashboard.DashboardTab) == 0 { 244 t.Errorf("Dashboard %v: - Must have more than one dashboardtab", dashboard.Name) 245 } 246 247 // dashboardtabSet is a set that checks duplicate tab name within each dashboard 248 dashboardtabSet := sets.NewString() 249 250 // dashboardtestgroupSet is a set that checks duplicate testgroups within each dashboard 251 dashboardtestgroupSet := sets.NewString() 252 253 // All notifications in dashboard must have a summary 254 if len(dashboard.Notifications) != 0 { 255 for notificationindex, notification := range dashboard.Notifications { 256 if notification.Summary == "" { 257 t.Errorf("Notification %v in dashboard %v: - Must have a summary", notificationindex, dashboard.Name) 258 } 259 } 260 } 261 262 for tabindex, dashboardtab := range dashboard.DashboardTab { 263 264 // All dashboardtab must have a name and a testgroup 265 if dashboardtab.Name == "" || dashboardtab.TestGroupName == "" { 266 t.Errorf("Dashboard %v, tab %v: - Must have a name and a testgroup name", dashboard.Name, tabindex) 267 } 268 269 // All dashboardtab within a dashboard must not have duplicated names 270 if dashboardtabSet.Has(dashboardtab.Name) { 271 t.Errorf("Duplicated name in dashboard %s: %v", dashboard.Name, dashboardtab.Name) 272 } else { 273 dashboardtabSet.Insert(dashboardtab.Name) 274 } 275 276 // All dashboardtab within a dashboard must not have duplicated testgroupnames 277 if dashboardtestgroupSet.Has(dashboardtab.TestGroupName) { 278 t.Errorf("Duplicated testgroupnames in dashboard %s: %v", dashboard.Name, dashboardtab.TestGroupName) 279 } else { 280 dashboardtestgroupSet.Insert(dashboardtab.TestGroupName) 281 } 282 283 // All testgroup in dashboard must be defined in testgroups 284 if testgroupMap[dashboardtab.TestGroupName] == 0 { 285 t.Errorf("Dashboard %v, tab %v: - Testgroup %v must be defined first", 286 dashboard.Name, dashboardtab.Name, dashboardtab.TestGroupName) 287 } else { 288 testgroupMap[dashboardtab.TestGroupName]++ 289 } 290 } 291 } 292 293 // No dup of dashboard groups, and no dup dashboard in a dashboard group 294 groupSet := sets.NewString() 295 dashboardToGroupMap := make(map[string]string) 296 297 for idx, dashboardGroup := range cfg.DashboardGroups { 298 // All dashboard must have a name 299 if dashboardGroup.Name == "" { 300 t.Errorf("DashboardGroup %v: - DashboardGroup must have a name", idx) 301 } 302 303 found := false 304 for _, kind := range dashboardPrefixes { 305 for _, prefix := range kind { 306 if strings.HasPrefix(dashboardGroup.Name, prefix) || prefix == dashboardGroup.Name { 307 found = true 308 break 309 } 310 } 311 if found { 312 break 313 } 314 } 315 if !found { 316 t.Errorf("Dashboard group %v: must prefix with one of: %v", dashboardGroup.Name, dashboardPrefixes) 317 } 318 319 // All dashboardgroup must not have duplicated names 320 if groupSet.Has(dashboardGroup.Name) { 321 t.Errorf("Duplicated dashboard: %v", dashboardGroup.Name) 322 } else { 323 groupSet.Insert(dashboardGroup.Name) 324 } 325 326 if dashboardSet.Has(dashboardGroup.Name) { 327 t.Errorf("%v is both a dashboard and dashboard group name.", dashboardGroup.Name) 328 } 329 330 for _, dashboard := range dashboardGroup.DashboardNames { 331 // All dashboard must not have duplicated names 332 if assignedGroup, ok := dashboardToGroupMap[dashboard]; ok { 333 t.Errorf("Duplicated dashboard %v in dashboard group %v and %v", dashboard, assignedGroup, dashboardGroup.Name) 334 } else { 335 dashboardToGroupMap[dashboard] = dashboardGroup.Name 336 } 337 338 if !dashboardSet.Has(dashboard) { 339 t.Errorf("Dashboard %v needs to be defined before adding to a dashboard group!", dashboard) 340 } 341 342 if !strings.HasPrefix(dashboard, dashboardGroup.Name) { 343 t.Errorf("Dashboard %v in group %v must have the group name as a prefix", dashboard, dashboardGroup.Name) 344 } 345 } 346 } 347 348 // Dashboards that match this dashboard group's prefix should be a part of it, unless this group is the prefix of the assigned group 349 // (e.g. knative and knative-sandbox). 350 for thisGroup := range groupSet { 351 for dashboard := range dashboardSet { 352 if strings.HasPrefix(dashboard, thisGroup+"-") { 353 assignedGroup, ok := dashboardToGroupMap[dashboard] 354 if !ok { 355 t.Errorf("Dashboard %v should be in dashboard_group %v", dashboard, thisGroup) 356 } else if assignedGroup != thisGroup && !strings.HasPrefix(assignedGroup, thisGroup) { 357 t.Errorf("Dashboard %v should be in dashboard_group %v instead of dashboard_group %v", dashboard, thisGroup, assignedGroup) 358 } 359 } 360 } 361 } 362 363 // All Testgroup should be mapped to one or more tabs 364 missedTestgroups := false 365 for testgroupname, occurrence := range testgroupMap { 366 if occurrence == 1 { 367 t.Errorf("Testgroup %v - defined but not used in any dashboards.", testgroupname) 368 missedTestgroups = true 369 } 370 } 371 if missedTestgroups { 372 t.Logf("Note: Testgroups are automatically defined for postsubmits and periodics.") 373 t.Logf("Testgroups can be added to a dashboard either by using the `testgrid-dashboards` annotation on the prowjob, or by adding them to testgrid/config.yaml") 374 } 375 } 376 377 // TODO: These are all repos that don't have their presubmits in testgrid. 378 // Convince sig leads or subproject owners this is a bad idea and whittle this down 379 // to just kubernetes-security/ 380 // Tracking issue: https://github.com/kubernetes/test-infra/issues/18159 381 var noPresubmitsInTestgridPrefixes = []string{ 382 "containerd/cri", 383 "kubernetes-sigs/cluster-capacity", 384 "kubernetes-sigs/gcp-filestore-csi-driver", 385 "kubernetes-sigs/kind", 386 "kubernetes-sigs/kubetest2", 387 "kubernetes-sigs/oci-proxy", 388 "kubernetes-sigs/kubebuilder-declarative-pattern", 389 "kubernetes-sigs/scheduler-plugins", 390 "kubernetes-sigs/service-catalog", 391 "kubernetes-sigs/sig-storage-local-static-provisioner", 392 "kubernetes-sigs/slack-infra", 393 "kubernetes-sigs/testing_frameworks", 394 "kubernetes/client-go", 395 "kubernetes/cloud-provider-openstack", 396 "kubernetes/dns", 397 "kubernetes/enhancements", 398 "kubernetes/ingress-gce", 399 "kubernetes/kubeadm", 400 "kubernetes/minikube", 401 // This is the one entry that should be here 402 "kubernetes-security/", 403 } 404 405 func hasAnyPrefix(s string, prefixes []string) bool { 406 for _, prefix := range prefixes { 407 if strings.HasPrefix(s, prefix) { 408 return true 409 } 410 } 411 return false 412 } 413 414 // A job is merge-blocking if it: 415 // - is not optional 416 // - reports (aka does not skip reporting) 417 // - always runs OR runs if some path changed 418 func isMergeBlocking(job prow_config.Presubmit) bool { 419 return !job.Optional && !job.SkipReport && (job.AlwaysRun || job.RunIfChanged != "" || job.SkipIfOnlyChanged != "") 420 } 421 422 // All jobs in presubmits-kubernetes-blocking must be merge-blocking for kubernetes/kubernetes 423 // All jobs that are merge-blocking for kubernetes/kubernetes must be in presubmits-kubernetes-blocking 424 func TestPresubmitsKubernetesDashboards(t *testing.T) { 425 var dashboard *config_pb.Dashboard 426 repo := "kubernetes/kubernetes" 427 dash := "presubmits-kubernetes-blocking" 428 for _, d := range cfg.Dashboards { 429 if d.Name == dash { 430 dashboard = d 431 } 432 } 433 if dashboard == nil { 434 t.Fatalf("Missing dashboard: %s", dash) 435 } 436 testgroups := make(map[string]bool) 437 for _, tab := range dashboard.DashboardTab { 438 testgroups[tab.TestGroupName] = false 439 } 440 jobs := make(map[string]bool) 441 for _, job := range prowConfig.AllStaticPresubmits([]string{repo}) { 442 if isMergeBlocking(job) { 443 jobs[job.Name] = false 444 } 445 } 446 for job, seen := range jobs { 447 if _, ok := testgroups[job]; !seen && !ok { 448 t.Errorf("%s: job is merge-blocking for %s but missing from %s", job, repo, dash) 449 } 450 jobs[job] = true 451 } 452 for tg, seen := range testgroups { 453 if _, ok := jobs[tg]; !seen && !ok { 454 t.Errorf("%s: should not be in %s because not actually merge-blocking for %s", tg, dash, repo) 455 } 456 testgroups[tg] = true 457 } 458 } 459 460 func TestKubernetesProwInstanceJobsMustHaveMatchingTestgridEntries(t *testing.T) { 461 jobs := make(map[string]bool) 462 463 for repo, presubmits := range prowConfig.PresubmitsStatic { 464 // Assume that all jobs in the exceptionList are valid 465 if hasAnyPrefix(repo, noPresubmitsInTestgridPrefixes) { 466 for _, job := range presubmits { 467 jobs[job.Name] = true 468 } 469 continue 470 } 471 for _, job := range presubmits { 472 jobs[job.Name] = false 473 } 474 } 475 476 for _, job := range prowConfig.AllStaticPostsubmits([]string{}) { 477 jobs[job.Name] = false 478 } 479 480 for _, job := range prowConfig.AllPeriodics() { 481 jobs[job.Name] = false 482 } 483 484 // Ignore any test groups that get their results from a gcs prefix 485 // that is not populated by the kubernetes prow instance 486 testgroups := make(map[string]bool) 487 for _, testgroup := range cfg.TestGroups { 488 for _, prowGcsPrefix := range prowGcsPrefixes { 489 if strings.Contains(testgroup.GcsPrefix, prowGcsPrefix) { 490 // The convention is that the job name is the final part of the GcsPrefix 491 job := filepath.Base(testgroup.GcsPrefix) 492 testgroups[job] = false 493 break // to next testgroup 494 } 495 } 496 } 497 498 // Each job running in the kubernetes prow instance must have an 499 // identically named test_groups entry in the kubernetes testgrid config 500 for job := range jobs { 501 if _, ok := testgroups[job]; ok { 502 testgroups[job] = true 503 jobs[job] = true 504 } 505 } 506 507 // Conclusion 508 for job, valid := range jobs { 509 if !valid { 510 t.Errorf("Job %v does not have a matching testgrid testgroup", job) 511 } 512 } 513 514 for testgroup, valid := range testgroups { 515 if !valid { 516 t.Errorf("Testgrid group %v is supposed to be moved to have their presubmits in testgrid. See this issue: https://github.com/kubernetes/test-infra/issues/18159", testgroup) 517 } 518 } 519 } 520 521 func TestReleaseBlockingJobsMustHaveTestgridDescriptions(t *testing.T) { 522 // TODO(spiffxp): start with master, enforce for all release branches 523 re := regexp.MustCompile("^sig-release-master-(blocking|informing)$") 524 for _, dashboard := range cfg.Dashboards { 525 if !re.MatchString(dashboard.Name) { 526 continue 527 } 528 suffix := re.FindStringSubmatch(dashboard.Name)[1] 529 for _, dashboardtab := range dashboard.DashboardTab { 530 intro := fmt.Sprintf("dashboard_tab %v/%v is release-%v", dashboard.Name, dashboardtab.Name, suffix) 531 if dashboardtab.Name == "" { 532 t.Errorf("%v: - Must have a name", intro) 533 } 534 if dashboardtab.TestGroupName == "" { 535 t.Errorf("%v: - Must have a test_group_name", intro) 536 } 537 if dashboardtab.Description == "" { 538 t.Errorf("%v: - Must have a description", intro) 539 } 540 // TODO(spiffxp): enforce for informing as well 541 if suffix == "informing" { 542 if !strings.HasPrefix(dashboardtab.Description, "OWNER: ") { 543 t.Logf("NOTICE: %v: - Must have a description that starts with OWNER: ", intro) 544 } 545 if dashboardtab.AlertOptions == nil { 546 t.Logf("NOTICE: %v: - Must have alert_options (ensure informing dashboard is listed first in testgrid-dashboards)", intro) 547 } else if dashboardtab.AlertOptions.AlertMailToAddresses == "" { 548 t.Logf("NOTICE: %v: - Must have alert_options.alert_mail_to_addresses", intro) 549 } 550 } else { 551 if dashboardtab.AlertOptions == nil { 552 t.Errorf("%v: - Must have alert_options (ensure blocking dashboard is listed first in testgrid-dashboards)", intro) 553 } else if dashboardtab.AlertOptions.AlertMailToAddresses == "" { 554 t.Errorf("%v: - Must have alert_options.alert_mail_to_addresses", intro) 555 } 556 } 557 } 558 } 559 } 560 561 func TestNoEmpyMailToAddresses(t *testing.T) { 562 for _, dashboard := range cfg.Dashboards { 563 for _, dashboardtab := range dashboard.DashboardTab { 564 intro := fmt.Sprintf("dashboard_tab %v/%v", dashboard.Name, dashboardtab.Name) 565 if dashboardtab.AlertOptions != nil { 566 if dashboardtab.AlertOptions.AlertMailToAddresses == "" { 567 continue 568 } 569 mails := strings.Split(dashboardtab.AlertOptions.AlertMailToAddresses, ",") 570 for _, m := range mails { 571 _, err := mail.ParseAddress(m) 572 if err != nil { 573 t.Errorf("%v: - invalid alert_mail_to_address '%v': %v", intro, m, err) 574 } 575 } 576 } 577 } 578 } 579 }