k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/config/tests/jobs/from_prow_test.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 tests
    18  
    19  import (
    20  	"regexp"
    21  	"testing"
    22  
    23  	v1 "k8s.io/api/core/v1"
    24  	"k8s.io/apimachinery/pkg/util/sets"
    25  	"sigs.k8s.io/prow/pkg/config"
    26  )
    27  
    28  // This file contains tests that previously lived in prow/config/jobs_test.go and prow/config/jobtests/job_config_test.go
    29  // Some (all?) of these tests are not specific to the K8s Prow instance and should
    30  // be ported to checkconfig so that all Prow instances can benefit from them.
    31  // TODO: move such validations to checkconfig if they are not present already.
    32  
    33  var podRe = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
    34  
    35  // Returns if two brancher has overlapping branches
    36  func checkOverlapBrancher(b1, b2 config.Brancher) bool {
    37  	if b1.RunsAgainstAllBranch() || b2.RunsAgainstAllBranch() {
    38  		return true
    39  	}
    40  
    41  	for _, run1 := range b1.Branches {
    42  		if b2.ShouldRun(run1) {
    43  			return true
    44  		}
    45  	}
    46  
    47  	for _, run2 := range b2.Branches {
    48  		if b1.ShouldRun(run2) {
    49  			return true
    50  		}
    51  	}
    52  
    53  	return false
    54  }
    55  
    56  func TestPresubmits(t *testing.T) {
    57  	if len(c.PresubmitsStatic) == 0 {
    58  		t.Fatalf("No jobs found in presubmit.yaml.")
    59  	}
    60  
    61  	for _, rootJobs := range c.PresubmitsStatic {
    62  		for i, job := range rootJobs {
    63  			if job.Name == "" {
    64  				t.Errorf("Job %v needs a name.", job)
    65  				continue
    66  			}
    67  			if !job.SkipReport && job.Context == "" {
    68  				t.Errorf("Job %s needs a context.", job.Name)
    69  			}
    70  			if job.RerunCommand == "" || job.Trigger == "" {
    71  				t.Errorf("Job %s needs a trigger and a rerun command.", job.Name)
    72  				continue
    73  			}
    74  
    75  			if len(job.Brancher.Branches) > 0 && len(job.Brancher.SkipBranches) > 0 {
    76  				t.Errorf("Job %s : Cannot have both branches and skip_branches set", job.Name)
    77  			}
    78  			// Next check that the rerun command doesn't run any other jobs.
    79  			for j, job2 := range rootJobs[i+1:] {
    80  				if job.Name == job2.Name {
    81  					// Make sure max_concurrency are the same
    82  					if job.MaxConcurrency != job2.MaxConcurrency {
    83  						t.Errorf("Jobs %s share same name but has different max_concurrency", job.Name)
    84  					}
    85  					// Make sure branches are not overlapping
    86  					if checkOverlapBrancher(job.Brancher, job2.Brancher) {
    87  						t.Errorf("Two jobs have the same name: %s, and have conflicting branches", job.Name)
    88  					}
    89  				} else {
    90  					if job.Context == job2.Context {
    91  						t.Errorf("Jobs %s and %s have the same context: %s", job.Name, job2.Name, job.Context)
    92  					}
    93  					if job2.TriggerMatches(job.RerunCommand) {
    94  						t.Errorf("%d, %d, RerunCommand \"%s\" from job %s matches \"%v\" from job %s but shouldn't.", i, j, job.RerunCommand, job.Name, job2.Trigger, job2.Name)
    95  					}
    96  				}
    97  			}
    98  		}
    99  	}
   100  }
   101  
   102  // TODO(krzyzacy): technically this, and TestPresubmits above should belong to config/ instead of prow/
   103  func TestPostsubmits(t *testing.T) {
   104  	if len(c.PostsubmitsStatic) == 0 {
   105  		t.Fatalf("No jobs found in presubmit.yaml.")
   106  	}
   107  
   108  	for _, rootJobs := range c.PostsubmitsStatic {
   109  		for i, job := range rootJobs {
   110  			if job.Name == "" {
   111  				t.Errorf("Job %v needs a name.", job)
   112  				continue
   113  			}
   114  			if !job.SkipReport && job.Context == "" {
   115  				t.Errorf("Job %s needs a context.", job.Name)
   116  			}
   117  
   118  			if len(job.Brancher.Branches) > 0 && len(job.Brancher.SkipBranches) > 0 {
   119  				t.Errorf("Job %s : Cannot have both branches and skip_branches set", job.Name)
   120  			}
   121  			// Next check that the rerun command doesn't run any other jobs.
   122  			for _, job2 := range rootJobs[i+1:] {
   123  				if job.Name == job2.Name {
   124  					// Make sure max_concurrency are the same
   125  					if job.MaxConcurrency != job2.MaxConcurrency {
   126  						t.Errorf("Jobs %s share same name but has different max_concurrency", job.Name)
   127  					}
   128  					// Make sure branches are not overlapping
   129  					if checkOverlapBrancher(job.Brancher, job2.Brancher) {
   130  						t.Errorf("Two jobs have the same name: %s, and have conflicting branches", job.Name)
   131  					}
   132  				} else {
   133  					if job.Context == job2.Context {
   134  						t.Errorf("Jobs %s and %s have the same context: %s", job.Name, job2.Name, job.Context)
   135  					}
   136  				}
   137  			}
   138  		}
   139  	}
   140  }
   141  
   142  func TestValidPodNames(t *testing.T) {
   143  	for _, j := range c.AllStaticPresubmits([]string{}) {
   144  		if !podRe.MatchString(j.Name) {
   145  			t.Errorf("Job \"%s\" must match regex \"%s\".", j.Name, podRe.String())
   146  		}
   147  	}
   148  	for _, j := range c.AllStaticPostsubmits([]string{}) {
   149  		if !podRe.MatchString(j.Name) {
   150  			t.Errorf("Job \"%s\" must match regex \"%s\".", j.Name, podRe.String())
   151  		}
   152  	}
   153  	for _, j := range c.AllPeriodics() {
   154  		if !podRe.MatchString(j.Name) {
   155  			t.Errorf("Job \"%s\" must match regex \"%s\".", j.Name, podRe.String())
   156  		}
   157  	}
   158  }
   159  
   160  func TestNoDuplicateJobs(t *testing.T) {
   161  	// Presubmit test is covered under TestPresubmits() above
   162  
   163  	allJobs := make(map[string]bool)
   164  	for _, j := range c.AllStaticPostsubmits([]string{}) {
   165  		if allJobs[j.Name] {
   166  			t.Errorf("Found duplicate job in postsubmit: %s.", j.Name)
   167  		}
   168  		allJobs[j.Name] = true
   169  	}
   170  
   171  	allJobs = make(map[string]bool)
   172  	for _, j := range c.AllPeriodics() {
   173  		if allJobs[j.Name] {
   174  			t.Errorf("Found duplicate job in periodic %s.", j.Name)
   175  		}
   176  		allJobs[j.Name] = true
   177  	}
   178  }
   179  
   180  func missingVolumesForContainer(mounts []v1.VolumeMount, volumes []v1.Volume) sets.Set[string] {
   181  	mountNames := sets.New[string]()
   182  	volumeNames := sets.New[string]()
   183  	for _, m := range mounts {
   184  		mountNames.Insert(m.Name)
   185  	}
   186  	for _, v := range volumes {
   187  		volumeNames.Insert(v.Name)
   188  	}
   189  	return mountNames.Difference(volumeNames)
   190  }
   191  
   192  func missingVolumesForSpec(spec *v1.PodSpec) map[string]sets.Set[string] {
   193  	malformed := map[string]sets.Set[string]{}
   194  	for _, container := range spec.InitContainers {
   195  		malformed[container.Name] = missingVolumesForContainer(container.VolumeMounts, spec.Volumes)
   196  	}
   197  	for _, container := range spec.Containers {
   198  		malformed[container.Name] = missingVolumesForContainer(container.VolumeMounts, spec.Volumes)
   199  	}
   200  	return malformed
   201  }
   202  
   203  func missingMountsForSpec(spec *v1.PodSpec) sets.Set[string] {
   204  	mountNames := sets.New[string]()
   205  	volumeNames := sets.New[string]()
   206  	for _, container := range spec.Containers {
   207  		for _, m := range container.VolumeMounts {
   208  			mountNames.Insert(m.Name)
   209  		}
   210  	}
   211  	for _, container := range spec.InitContainers {
   212  		for _, m := range container.VolumeMounts {
   213  			mountNames.Insert(m.Name)
   214  		}
   215  	}
   216  	for _, v := range spec.Volumes {
   217  		volumeNames.Insert(v.Name)
   218  	}
   219  	return volumeNames.Difference(mountNames)
   220  }
   221  
   222  // verify that all volume mounts reference volumes that exist
   223  func TestMountsHaveVolumes(t *testing.T) {
   224  	for _, job := range c.AllStaticPresubmits(nil) {
   225  		if job.Spec != nil {
   226  			validateVolumesAndMounts(job.Name, job.Spec, t)
   227  		}
   228  	}
   229  	for _, job := range c.AllStaticPostsubmits(nil) {
   230  		if job.Spec != nil {
   231  			validateVolumesAndMounts(job.Name, job.Spec, t)
   232  		}
   233  	}
   234  	for _, job := range c.AllPeriodics() {
   235  		if job.Spec != nil {
   236  			validateVolumesAndMounts(job.Name, job.Spec, t)
   237  		}
   238  	}
   239  }
   240  
   241  func validateVolumesAndMounts(name string, spec *v1.PodSpec, t *testing.T) {
   242  	for container, missingVolumes := range missingVolumesForSpec(spec) {
   243  		if len(missingVolumes) > 0 {
   244  			t.Errorf("job %s in container %s has mounts that are missing volumes: %v", name, container, sets.List(missingVolumes))
   245  		}
   246  	}
   247  	if missingMounts := missingMountsForSpec(spec); len(missingMounts) > 0 {
   248  		t.Errorf("job %s has volumes that are not mounted: %v", name, sets.List(missingMounts))
   249  	}
   250  }