| 
 | 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 policy  | 
 | 18 | + | 
 | 19 | +// This file validates Kubernetes's jobs configs against policies.  | 
 | 20 | + | 
 | 21 | +import (  | 
 | 22 | +	"bytes"  | 
 | 23 | +	"flag"  | 
 | 24 | +	"fmt"  | 
 | 25 | +	"os"  | 
 | 26 | +	"path/filepath"  | 
 | 27 | +	"slices"  | 
 | 28 | +	"sort"  | 
 | 29 | +	"strings"  | 
 | 30 | +	"testing"  | 
 | 31 | + | 
 | 32 | +	"github.com/google/go-cmp/cmp"  | 
 | 33 | +	"github.com/google/go-cmp/cmp/cmpopts"  | 
 | 34 | +	yaml "sigs.k8s.io/yaml/goyaml.v3"  | 
 | 35 | + | 
 | 36 | +	cfg "sigs.k8s.io/prow/pkg/config"  | 
 | 37 | +)  | 
 | 38 | + | 
 | 39 | +var configPath = flag.String("config", "../../../../config/prow/config.yaml", "Path to prow config")  | 
 | 40 | +var jobConfigPath = flag.String("job-config", "../../../jobs", "Path to prow job config")  | 
 | 41 | + | 
 | 42 | +// Loaded at TestMain.  | 
 | 43 | +var c *cfg.Config  | 
 | 44 | + | 
 | 45 | +func TestMain(m *testing.M) {  | 
 | 46 | +	flag.Parse()  | 
 | 47 | +	if *configPath == "" {  | 
 | 48 | +		fmt.Println("--config must set")  | 
 | 49 | +		os.Exit(1)  | 
 | 50 | +	}  | 
 | 51 | + | 
 | 52 | +	conf, err := cfg.Load(*configPath, *jobConfigPath, nil, "")  | 
 | 53 | +	if err != nil {  | 
 | 54 | +		fmt.Printf("Could not load config: %v", err)  | 
 | 55 | +		os.Exit(1)  | 
 | 56 | +	}  | 
 | 57 | +	c = conf  | 
 | 58 | + | 
 | 59 | +	os.Exit(m.Run())  | 
 | 60 | +}  | 
 | 61 | + | 
 | 62 | +func TestKubernetesPresubmitJobs(t *testing.T) {  | 
 | 63 | +	jobs := c.AllStaticPresubmits([]string{"kubernetes/kubernetes"})  | 
 | 64 | +	var expected presubmitJobs  | 
 | 65 | + | 
 | 66 | +	for _, job := range jobs {  | 
 | 67 | +		if !job.AlwaysRun && job.RunIfChanged == "" {  | 
 | 68 | +			// Manually triggered, no additional review needed.  | 
 | 69 | +			continue  | 
 | 70 | +		}  | 
 | 71 | + | 
 | 72 | +		// Mirror those attributes of the job which must trigger additional reviews  | 
 | 73 | +		// or are needed to identify the job.  | 
 | 74 | +		j := presubmitJob{  | 
 | 75 | +			Name:         job.Name,  | 
 | 76 | +			SkipBranches: job.SkipBranches,  | 
 | 77 | +			Branches:     job.Branches,  | 
 | 78 | + | 
 | 79 | +			RunIfChanged:      job.RunIfChanged,  | 
 | 80 | +			SkipIfOnlyChanged: job.SkipIfOnlyChanged,  | 
 | 81 | +		}  | 
 | 82 | + | 
 | 83 | +		// This uses separate top-level fields instead of job attributes to  | 
 | 84 | +		// make it more obvious when run_if_changed is used.  | 
 | 85 | +		if job.AlwaysRun {  | 
 | 86 | +			expected.AlwaysRun = append(expected.AlwaysRun, j)  | 
 | 87 | +		} else {  | 
 | 88 | +			expected.RunIfChanged = append(expected.RunIfChanged, j)  | 
 | 89 | + | 
 | 90 | +			if !job.Optional {  | 
 | 91 | +				// Absolute path is more user-friendly than ../../config/...  | 
 | 92 | +				t.Errorf("Policy violation: %s in %s should use `optional: true` or `alwaysRun: true`.", job.Name, maybeAbsPath(job.SourcePath))  | 
 | 93 | +			}  | 
 | 94 | +		}  | 
 | 95 | + | 
 | 96 | +	}  | 
 | 97 | +	expected.Normalize()  | 
 | 98 | + | 
 | 99 | +	// Encode the expected content.  | 
 | 100 | +	var expectedData bytes.Buffer  | 
 | 101 | +	if _, err := expectedData.Write([]byte(`# AUTOGENERATED by "UPDATE_FIXTURE_DATA=true go test ./config/tests/jobs". DO NOT EDIT!  | 
 | 102 | +
  | 
 | 103 | +`)); err != nil {  | 
 | 104 | +		t.Fatalf("unexpected error writing into buffer: %v", err)  | 
 | 105 | +	}  | 
 | 106 | + | 
 | 107 | +	encoder := yaml.NewEncoder(&expectedData)  | 
 | 108 | +	encoder.SetIndent(4)  | 
 | 109 | +	if err := encoder.Encode(expected); err != nil {  | 
 | 110 | +		t.Fatalf("unexpected error encoding %s: %v", presubmitsFile, err)  | 
 | 111 | +	}  | 
 | 112 | + | 
 | 113 | +	// Compare. This proceeds on read or decoding errors because  | 
 | 114 | +	// the file might get re-generated below.  | 
 | 115 | +	var actual presubmitJobs  | 
 | 116 | +	actualData, err := os.ReadFile(presubmitsFile)  | 
 | 117 | +	if err != nil && !os.IsNotExist(err) {  | 
 | 118 | +		t.Errorf("unexpected error: %v", err)  | 
 | 119 | +	}  | 
 | 120 | +	if err := yaml.Unmarshal(actualData, &actual); err != nil {  | 
 | 121 | +		t.Errorf("unexpected error decoding %s: %v", presubmitsFile, err)  | 
 | 122 | +	}  | 
 | 123 | + | 
 | 124 | +	// First check the in-memory structs. The diff is nicer for them (more context).  | 
 | 125 | +	diff := cmp.Diff(actual, expected)  | 
 | 126 | +	if diff == "" {  | 
 | 127 | +		// Next check the encoded data. This should only be different on test updates.  | 
 | 128 | +		diff = cmp.Diff(string(actualData), expectedData.String(), cmpopts.AcyclicTransformer("SplitLines", func(s string) []string {  | 
 | 129 | +			return strings.Split(s, "\n")  | 
 | 130 | +		}))  | 
 | 131 | +	}  | 
 | 132 | + | 
 | 133 | +	if diff != "" {  | 
 | 134 | +		if value, _ := os.LookupEnv("UPDATE_FIXTURE_DATA"); value == "true" {  | 
 | 135 | +			if err := os.WriteFile(presubmitsFile, expectedData.Bytes(), 0644); err != nil {  | 
 | 136 | +				t.Fatalf("unexpected error: %v", err)  | 
 | 137 | +			}  | 
 | 138 | +			t.Logf(`  | 
 | 139 | +%s was out-dated. Updated as requested with the following changes (- actual, + expected):  | 
 | 140 | +%s  | 
 | 141 | +`, maybeAbsPath(presubmitsFile), diff)  | 
 | 142 | +		} else {  | 
 | 143 | +			t.Errorf(`  | 
 | 144 | +%s is out-dated. Detected differences (- actual, + expected):  | 
 | 145 | +%s  | 
 | 146 | +
  | 
 | 147 | +Blocking pre-submit jobs must be for stable, important features.  | 
 | 148 | +Non-blocking pre-submit jobs should only be run automatically if they meet  | 
 | 149 | +the criteria outlined in https://github.com/kubernetes/community/pull/8196.  | 
 | 150 | +
  | 
 | 151 | +To ensure that this is considered when defining pre-submit jobs, they  | 
 | 152 | +need to be listed in %s. If the pre-submit job is really needed,  | 
 | 153 | +re-run the test with UPDATE_FIXTURE_DATA=true and include the modified  | 
 | 154 | +file. The following command can be used:  | 
 | 155 | +
  | 
 | 156 | +   make update-config-fixture  | 
 | 157 | +`, presubmitsFile, diff, presubmitsFile)  | 
 | 158 | +		}  | 
 | 159 | +	}  | 
 | 160 | +}  | 
 | 161 | + | 
 | 162 | +// presubmitsFile contains the following struct.  | 
 | 163 | +const presubmitsFile = "presubmit-jobs.yaml"  | 
 | 164 | + | 
 | 165 | +type presubmitJobs struct {  | 
 | 166 | +	AlwaysRun    []presubmitJob `yaml:"always_run"`  | 
 | 167 | +	RunIfChanged []presubmitJob `yaml:"run_if_changed"`  | 
 | 168 | +}  | 
 | 169 | +type presubmitJob struct {  | 
 | 170 | +	Name              string   `yaml:"name"`  | 
 | 171 | +	SkipBranches      []string `yaml:"skip_branches,omitempty"`  | 
 | 172 | +	Branches          []string `yaml:"branches,omitempty"`  | 
 | 173 | +	RunIfChanged      string   `yaml:"run_if_changed,omitempty"`  | 
 | 174 | +	SkipIfOnlyChanged string   `yaml:"skip_if_only_changed,omitempty"`  | 
 | 175 | +}  | 
 | 176 | + | 
 | 177 | +func (p *presubmitJobs) Normalize() {  | 
 | 178 | +	sortJobs(&p.AlwaysRun)  | 
 | 179 | +	sortJobs(&p.RunIfChanged)  | 
 | 180 | +}  | 
 | 181 | + | 
 | 182 | +func sortJobs(jobs *[]presubmitJob) {  | 
 | 183 | +	for _, job := range *jobs {  | 
 | 184 | +		sort.Strings(job.SkipBranches)  | 
 | 185 | +		sort.Strings(job.Branches)  | 
 | 186 | +	}  | 
 | 187 | +	sort.Slice(*jobs, func(i, j int) bool {  | 
 | 188 | +		switch strings.Compare((*jobs)[i].Name, (*jobs)[j].Name) {  | 
 | 189 | +		case -1:  | 
 | 190 | +			return true  | 
 | 191 | +		case 1:  | 
 | 192 | +			return false  | 
 | 193 | +		}  | 
 | 194 | +		switch slices.Compare((*jobs)[i].SkipBranches, (*jobs)[j].SkipBranches) {  | 
 | 195 | +		case -1:  | 
 | 196 | +			return true  | 
 | 197 | +		case 1:  | 
 | 198 | +			return false  | 
 | 199 | +		}  | 
 | 200 | +		switch slices.Compare((*jobs)[i].Branches, (*jobs)[j].Branches) {  | 
 | 201 | +		case -1:  | 
 | 202 | +			return true  | 
 | 203 | +		case 1:  | 
 | 204 | +			return false  | 
 | 205 | +		}  | 
 | 206 | +		return false  | 
 | 207 | +	})  | 
 | 208 | + | 
 | 209 | +	// If a job has the same settings regardless of the branch, then  | 
 | 210 | +	// we can reduce to a single entry without the branch info.  | 
 | 211 | +	shorterJobs := make([]presubmitJob, 0, len(*jobs))  | 
 | 212 | +	for i := 0; i < len(*jobs); {  | 
 | 213 | +		job := (*jobs)[i]  | 
 | 214 | +		job.Branches = nil  | 
 | 215 | +		job.SkipBranches = nil  | 
 | 216 | + | 
 | 217 | +		if sameSettings(*jobs, job) {  | 
 | 218 | +			shorterJobs = append(shorterJobs, job)  | 
 | 219 | +			// Fast-forward to next job.  | 
 | 220 | +			for i < len(*jobs) && (*jobs)[i].Name == job.Name {  | 
 | 221 | +				i++  | 
 | 222 | +			}  | 
 | 223 | +		} else {  | 
 | 224 | +			// Keep all of the different entries.  | 
 | 225 | +			for i < len(*jobs) && (*jobs)[i].Name == job.Name {  | 
 | 226 | +				shorterJobs = append(shorterJobs, (*jobs)[i])  | 
 | 227 | +			}  | 
 | 228 | +		}  | 
 | 229 | +	}  | 
 | 230 | +	*jobs = shorterJobs  | 
 | 231 | +}  | 
 | 232 | + | 
 | 233 | +func sameSettings(jobs []presubmitJob, ref presubmitJob) bool {  | 
 | 234 | +	for _, job := range jobs {  | 
 | 235 | +		if job.Name != ref.Name {  | 
 | 236 | +			continue  | 
 | 237 | +		}  | 
 | 238 | +		if job.RunIfChanged != ref.RunIfChanged ||  | 
 | 239 | +			job.SkipIfOnlyChanged != ref.SkipIfOnlyChanged {  | 
 | 240 | +			return false  | 
 | 241 | +		}  | 
 | 242 | +	}  | 
 | 243 | +	return true  | 
 | 244 | +}  | 
 | 245 | + | 
 | 246 | +// maybeAbsPath tries to make a path absolute. This is useful because  | 
 | 247 | +// relative paths in test output tend to be confusing when the user  | 
 | 248 | +// invoked the test outside of the test's directory.  | 
 | 249 | +func maybeAbsPath(path string) string {  | 
 | 250 | +	if path, err := filepath.Abs(path); err == nil {  | 
 | 251 | +		return path  | 
 | 252 | +	}  | 
 | 253 | +	return path  | 
 | 254 | +}  | 
0 commit comments