sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/cmd/checkconfig/main_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 main 18 19 import ( 20 "context" 21 "flag" 22 "fmt" 23 stdio "io" 24 "os" 25 "path/filepath" 26 "reflect" 27 "regexp" 28 "strings" 29 "testing" 30 "testing/fstest" 31 "time" 32 33 "github.com/google/go-cmp/cmp" 34 "k8s.io/apimachinery/pkg/util/diff" 35 utilerrors "k8s.io/apimachinery/pkg/util/errors" 36 "k8s.io/apimachinery/pkg/util/sets" 37 utilpointer "k8s.io/utils/pointer" 38 "sigs.k8s.io/yaml" 39 40 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 41 "sigs.k8s.io/prow/pkg/config" 42 "sigs.k8s.io/prow/pkg/flagutil" 43 configflagutil "sigs.k8s.io/prow/pkg/flagutil/config" 44 pluginsflagutil "sigs.k8s.io/prow/pkg/flagutil/plugins" 45 "sigs.k8s.io/prow/pkg/github" 46 "sigs.k8s.io/prow/pkg/io" 47 "sigs.k8s.io/prow/pkg/plank" 48 "sigs.k8s.io/prow/pkg/plugins" 49 ) 50 51 func TestEnsureValidConfiguration(t *testing.T) { 52 var testCases = []struct { 53 name string 54 tideSubSet, tideSuperSet, pluginsSubSet *orgRepoConfig 55 expectedErr bool 56 }{ 57 { 58 name: "nothing enabled makes no error", 59 tideSubSet: newOrgRepoConfig(nil, nil), 60 tideSuperSet: newOrgRepoConfig(nil, nil), 61 pluginsSubSet: newOrgRepoConfig(nil, nil), 62 expectedErr: false, 63 }, 64 { 65 name: "plugin enabled on org without tide makes no error", 66 tideSubSet: newOrgRepoConfig(nil, nil), 67 tideSuperSet: newOrgRepoConfig(nil, nil), 68 pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 69 expectedErr: false, 70 }, 71 { 72 name: "plugin enabled on repo without tide makes no error", 73 tideSubSet: newOrgRepoConfig(nil, nil), 74 tideSuperSet: newOrgRepoConfig(nil, nil), 75 pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 76 expectedErr: false, 77 }, 78 { 79 name: "plugin enabled on repo with tide on repo makes no error", 80 tideSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 81 tideSuperSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 82 pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 83 expectedErr: false, 84 }, 85 { 86 name: "plugin enabled on repo with tide on org makes error", 87 tideSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 88 tideSuperSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 89 pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 90 expectedErr: true, 91 }, 92 { 93 name: "plugin enabled on org with tide on repo makes no error", 94 tideSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 95 tideSuperSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 96 pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 97 expectedErr: false, 98 }, 99 { 100 name: "plugin enabled on org with tide on org makes no error", 101 tideSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 102 tideSuperSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 103 pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 104 expectedErr: false, 105 }, 106 { 107 name: "tide enabled on org without plugin makes error", 108 tideSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 109 tideSuperSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 110 pluginsSubSet: newOrgRepoConfig(nil, nil), 111 expectedErr: true, 112 }, 113 { 114 name: "tide enabled on repo without plugin makes error", 115 tideSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 116 tideSuperSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 117 pluginsSubSet: newOrgRepoConfig(nil, nil), 118 expectedErr: true, 119 }, 120 { 121 name: "plugin enabled on org with any tide record but no specific tide requirement makes error", 122 tideSubSet: newOrgRepoConfig(nil, nil), 123 tideSuperSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 124 pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 125 expectedErr: true, 126 }, 127 { 128 name: "plugin enabled on repo with any tide record but no specific tide requirement makes error", 129 tideSubSet: newOrgRepoConfig(nil, nil), 130 tideSuperSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 131 pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 132 expectedErr: true, 133 }, 134 { 135 name: "any tide org record but no specific tide requirement or plugin makes no error", 136 tideSubSet: newOrgRepoConfig(nil, nil), 137 tideSuperSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 138 pluginsSubSet: newOrgRepoConfig(nil, nil), 139 expectedErr: false, 140 }, 141 { 142 name: "any tide repo record but no specific tide requirement or plugin makes no error", 143 tideSubSet: newOrgRepoConfig(nil, nil), 144 tideSuperSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 145 pluginsSubSet: newOrgRepoConfig(nil, nil), 146 expectedErr: false, 147 }, 148 { 149 name: "irrelevant repo exception in tide superset doesn't stop missing req error", 150 tideSubSet: newOrgRepoConfig(nil, nil), 151 tideSuperSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, nil), 152 pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 153 expectedErr: true, 154 }, 155 { 156 name: "repo exception in tide superset (no missing req error)", 157 tideSubSet: newOrgRepoConfig(nil, nil), 158 tideSuperSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, nil), 159 pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")), 160 expectedErr: false, 161 }, 162 { 163 name: "repo exception in tide subset (new missing req error)", 164 tideSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, nil), 165 tideSuperSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 166 pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil), 167 expectedErr: true, 168 }, 169 } 170 171 for _, testCase := range testCases { 172 t.Run(testCase.name, func(t *testing.T) { 173 err := ensureValidConfiguration("plugin", "label", "verb", testCase.tideSubSet, testCase.tideSuperSet, testCase.pluginsSubSet) 174 if testCase.expectedErr && err == nil { 175 t.Errorf("%s: expected an error but got none", testCase.name) 176 } 177 if !testCase.expectedErr && err != nil { 178 t.Errorf("%s: expected no error but got one: %v", testCase.name, err) 179 } 180 }) 181 } 182 } 183 184 func TestOrgRepoDifference(t *testing.T) { 185 testCases := []struct { 186 name string 187 a, b, expected *orgRepoConfig 188 }{ 189 { 190 name: "subtract nil", 191 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 192 b: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()), 193 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 194 }, 195 { 196 name: "no overlap", 197 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 198 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("3/1")), 199 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 200 }, 201 { 202 name: "subtract self", 203 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 204 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 205 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()), 206 }, 207 { 208 name: "subtract superset", 209 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 210 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "org2": nil}, sets.New[string]("4/1", "4/2", "5/1")), 211 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()), 212 }, 213 { 214 name: "remove org with org", 215 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo")}, sets.New[string]("4/1", "4/2")), 216 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/foo"), "2": nil}, sets.New[string]("3/1")), 217 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/1", "4/2")), 218 }, 219 { 220 name: "shrink org with org", 221 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 222 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo"), "2": nil}, sets.New[string]("3/1")), 223 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("org/foo", "4/1", "4/2")), 224 }, 225 { 226 name: "shrink org with repo", 227 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 228 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("org/foo", "3/1")), 229 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo")}, sets.New[string]("4/1", "4/2")), 230 }, 231 { 232 name: "remove repo with org", 233 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2", "4/3", "5/1")), 234 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": nil, "4": sets.New[string]("4/2")}, sets.New[string]("3/1")), 235 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/2", "5/1")), 236 }, 237 { 238 name: "remove repo with repo", 239 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2", "4/3", "5/1")), 240 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("3/1", "4/2", "4/3")), 241 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "5/1")), 242 }, 243 } 244 245 for _, tc := range testCases { 246 t.Run(tc.name, func(t *testing.T) { 247 got := tc.a.difference(tc.b) 248 if !reflect.DeepEqual(got, tc.expected) { 249 t.Errorf("expected config: %#v, but got config: %#v", tc.expected, got) 250 } 251 }) 252 } 253 } 254 255 func TestOrgRepoIntersection(t *testing.T) { 256 testCases := []struct { 257 name string 258 a, b, expected *orgRepoConfig 259 }{ 260 { 261 name: "intersect empty", 262 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 263 b: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()), 264 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()), 265 }, 266 { 267 name: "no overlap", 268 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 269 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("3/1")), 270 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()), 271 }, 272 { 273 name: "intersect self", 274 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 275 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 276 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 277 }, 278 { 279 name: "intersect superset", 280 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 281 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "org2": nil}, sets.New[string]("4/1", "4/2", "5/1")), 282 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 283 }, 284 { 285 name: "remove org", 286 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo")}, sets.New[string]("4/1", "4/2")), 287 b: newOrgRepoConfig(map[string]sets.Set[string]{"org2": sets.New[string]("org2/repo1")}, sets.New[string]("4/1", "4/2", "5/1")), 288 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/1", "4/2")), 289 }, 290 { 291 name: "shrink org with org", 292 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/bar")}, sets.New[string]("4/1", "4/2")), 293 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo"), "2": nil}, sets.New[string]("3/1")), 294 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo", "org/bar")}, sets.New[string]()), 295 }, 296 { 297 name: "shrink org with repo", 298 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 299 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("org/repo", "org/foo", "3/1", "4/1")), 300 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("org/foo", "4/1")), 301 }, 302 { 303 name: "remove repo with org", 304 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2", "4/3", "5/1")), 305 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": nil, "4": sets.New[string]("4/2")}, sets.New[string]("3/1")), 306 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/1", "4/3")), 307 }, 308 { 309 name: "remove repo with repo", 310 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2", "4/3", "5/1")), 311 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("3/1", "4/2", "4/3")), 312 expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/2", "4/3")), 313 }, 314 } 315 316 for _, tc := range testCases { 317 t.Run(tc.name, func(t *testing.T) { 318 got := tc.a.intersection(tc.b) 319 if !reflect.DeepEqual(got, tc.expected) { 320 t.Errorf("expected config: %#v, but got config: %#v", tc.expected, got) 321 } 322 }) 323 } 324 } 325 326 func TestOrgRepoUnion(t *testing.T) { 327 testCases := []struct { 328 name string 329 a, b, expected *orgRepoConfig 330 }{ 331 { 332 name: "second set empty, get first set back", 333 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 334 b: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()), 335 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 336 }, 337 { 338 name: "no overlap, simple union", 339 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 340 b: newOrgRepoConfig(map[string]sets.Set[string]{"2": sets.New[string]()}, sets.New[string]("3/1")), 341 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "2": sets.New[string]()}, sets.New[string]("4/1", "4/2", "3/1")), 342 }, 343 { 344 name: "union self, get self back", 345 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 346 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 347 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 348 }, 349 { 350 name: "union superset, get superset back", 351 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")), 352 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "org2": sets.New[string]()}, sets.New[string]("4/1", "4/2", "5/1")), 353 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "org2": sets.New[string]()}, sets.New[string]("4/1", "4/2", "5/1")), 354 }, 355 { 356 name: "keep only common denied items for an org", 357 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/bar")}, sets.New[string]()), 358 b: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo")}, sets.New[string]()), 359 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]()), 360 }, 361 { 362 name: "remove items from an org denylist if they're in a repo allowlist", 363 a: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]()), 364 b: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("org/repo")), 365 expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]()}, sets.New[string]()), 366 }, 367 { 368 name: "remove repos when they're covered by an org allowlist", 369 a: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/1", "4/2", "4/3")), 370 b: newOrgRepoConfig(map[string]sets.Set[string]{"4": sets.New[string]("4/2")}, sets.New[string]()), 371 expected: newOrgRepoConfig(map[string]sets.Set[string]{"4": sets.New[string]()}, sets.New[string]()), 372 }, 373 } 374 375 for _, tc := range testCases { 376 t.Run(tc.name, func(t *testing.T) { 377 got := tc.a.union(tc.b) 378 if !reflect.DeepEqual(got, tc.expected) { 379 t.Errorf("%s: did not get expected config:\n%v", tc.name, cmp.Diff(tc.expected, got)) 380 } 381 }) 382 } 383 } 384 385 func TestValidateUnknownFields(t *testing.T) { 386 testCases := []struct { 387 name, filename string 388 cfg interface{} 389 configBytes []byte 390 expectedErr string 391 }{ 392 { 393 name: "valid config", 394 filename: "valid-conf.yaml", 395 cfg: &plugins.Configuration{}, 396 configBytes: []byte(`plugins: 397 kube/kube: 398 - size 399 - config-updater 400 config_updater: 401 maps: 402 # Update the plugins configmap whenever plugins.yaml changes 403 kube/plugins.yaml: 404 name: plugins 405 size: 406 s: 1`), 407 expectedErr: "", 408 }, 409 { 410 name: "invalid top-level property", 411 filename: "toplvl.yaml", 412 cfg: &plugins.Configuration{}, 413 configBytes: []byte(`plugins: 414 kube/kube: 415 - size 416 - config-updater 417 notconfig_updater: 418 maps: 419 # Update the plugins configmap whenever plugins.yaml changes 420 kube/plugins.yaml: 421 name: plugins 422 size: 423 s: 1`), 424 expectedErr: "notconfig_updater", 425 }, 426 { 427 name: "invalid second-level property", 428 filename: "seclvl.yaml", 429 cfg: &plugins.Configuration{}, 430 configBytes: []byte(`plugins: 431 kube/kube: 432 - size 433 - config-updater 434 size: 435 xs: 1 436 s: 5`), 437 expectedErr: "xs", 438 }, 439 { 440 name: "invalid array element", 441 filename: "home/array.yaml", 442 cfg: &plugins.Configuration{}, 443 configBytes: []byte(`plugins: 444 kube/kube: 445 - size 446 - trigger 447 triggers: 448 - repos: 449 - kube/kube 450 - repoz: 451 - kube/kubez`), 452 expectedErr: "repoz", 453 }, 454 // Options like DisallowUnknownFields can not be passed when using 455 // a custon json.Unmarshaler like we do here for defaulting: 456 // https://github.com/golang/go/issues/41144 457 // { 458 // name: "invalid map entry", 459 // filename: "map.yaml", 460 // cfg: &plugins.Configuration{}, 461 // configBytes: []byte(`plugins: 462 // kube/kube: 463 // - size 464 // - config-updater 465 // config_updater: 466 // maps: 467 // # Update the plugins configmap whenever plugins.yaml changes 468 // kube/plugins.yaml: 469 // name: plugins 470 // kube/config.yaml: 471 // validation: config 472 // size: 473 // s: 1`), 474 // expectedErr: "validation", 475 // }, 476 { 477 // only one invalid element is printed in the error 478 name: "multiple invalid elements", 479 filename: "multiple.yaml", 480 cfg: &plugins.Configuration{}, 481 configBytes: []byte(`plugins: 482 kube/kube: 483 - size 484 - trigger 485 triggers: 486 - repoz: 487 - kube/kubez 488 - repos: 489 - kube/kube 490 size: 491 s: 1 492 xs: 1`), 493 expectedErr: "xs", 494 }, 495 { 496 name: "embedded structs - kube", 497 filename: "embedded.yaml", 498 cfg: &config.Config{}, 499 configBytes: []byte(`presubmits: 500 kube/kube: 501 - name: test-presubmit 502 decorate: true 503 always_run: true 504 never_run: false 505 skip_report: true 506 spec: 507 containers: 508 - image: alpine 509 command: ["/bin/printenv"]`), 510 expectedErr: "never_run", 511 }, 512 { 513 name: "embedded structs - tide", 514 filename: "embedded.yaml", 515 cfg: &config.Config{}, 516 configBytes: []byte(`tide: 517 squash_label: sq 518 not-a-property: true`), 519 expectedErr: "not-a-property", 520 }, 521 { 522 name: "embedded structs - size", 523 filename: "embedded.yaml", 524 cfg: &config.Config{}, 525 configBytes: []byte(`size: 526 s: 1 527 xs: 1`), 528 expectedErr: "size", 529 }, 530 { 531 name: "pointer to a slice", 532 filename: "pointer.yaml", 533 cfg: &plugins.Configuration{}, 534 configBytes: []byte(`bugzilla: 535 default: 536 '*': 537 statuses: 538 - foobar 539 extra: oops`), 540 expectedErr: "extra", 541 }, 542 } 543 544 for i := range testCases { 545 tc := testCases[i] 546 t.Run(tc.name, func(t *testing.T) { 547 t.Parallel() 548 if err := yaml.Unmarshal(tc.configBytes, tc.cfg); err != nil { 549 t.Fatalf("Unable to unmarhsal yaml: %v", err) 550 } 551 got := validateUnknownFields(tc.cfg, tc.configBytes, tc.filename) 552 553 if tc.expectedErr == "" { 554 if got != nil { 555 t.Errorf("%s: expected nil error but got:\n%v", tc.name, got) 556 } 557 } else { // check substrings in case yaml lib changes err fmt 558 var errMsg string 559 if got != nil { 560 errMsg = got.Error() 561 } 562 for _, s := range []string{"unknown field", tc.filename, tc.expectedErr} { 563 if !strings.Contains(errMsg, s) { 564 t.Errorf("%s: did not get expected validation error: expected substring in error message:\n%s\n but got:\n%v", tc.name, s, got) 565 } 566 } 567 } 568 }) 569 } 570 } 571 572 func TestValidateUnknownFieldsAll(t *testing.T) { 573 testcases := []struct { 574 name string 575 configContent string 576 jobConfigContent map[string]string 577 expectedErr bool 578 }{ 579 { 580 name: "no separate job-config, all known fields", 581 configContent: ` 582 plank: 583 default_decoration_config_entries: 584 - config: 585 timeout: 2h 586 grace_period: 15s 587 utility_images: 588 clonerefs: "clonerefs:default" 589 initupload: "initupload:default" 590 entrypoint: "entrypoint:default" 591 sidecar: "sidecar:default" 592 gcs_configuration: 593 bucket: "default-bucket" 594 path_strategy: "legacy" 595 default_org: "kubernetes" 596 default_repo: "kubernetes" 597 gcs_credentials_secret: "default-service-account" 598 599 presubmits: 600 kube/kube: 601 - name: test-presubmit 602 decorate: true 603 spec: 604 containers: 605 - image: alpine 606 command: ["/bin/printenv"] 607 `, 608 }, 609 { 610 name: "no separate job-config, unknown field", 611 configContent: ` 612 presubmits: 613 kube/kube: 614 - name: test-presubmit 615 never_run: true // I'm unknown 616 spec: 617 containers: 618 - image: alpine 619 `, 620 expectedErr: true, 621 }, 622 { 623 name: "separate job-configs, all known field", 624 configContent: ` 625 presubmits: 626 kube/kube: 627 - name: kube-presubmit 628 run_if_changed: "^src/" 629 spec: 630 containers: 631 - image: alpine 632 `, 633 jobConfigContent: map[string]string{ 634 "org-repo-presubmits.yaml": ` 635 presubmits: 636 org/repo: 637 - name: org-repo-presubmit 638 always_run: true 639 spec: 640 containers: 641 - image: alpine 642 `, 643 "org-repo2-presubmits.yaml": ` 644 presubmits: 645 org/repo2: 646 - name: org-repo2-presubmit 647 always_run: true 648 spec: 649 containers: 650 - image: alpine 651 `, 652 }, 653 }, 654 { 655 name: "separate job-configs, unknown field in second job config", 656 configContent: ` 657 presubmits: 658 kube/kube: 659 - name: kube-presubmit 660 never_run: true // I'm unknown 661 spec: 662 containers: 663 - image: alpine 664 `, 665 jobConfigContent: map[string]string{ 666 "org-repo-presubmits.yaml": ` 667 presubmits: 668 org/repo: 669 - name: org-repo-presubmit 670 always_run: true 671 spec: 672 containers: 673 - image: alpine 674 `, 675 "org-repo2-presubmits.yaml": ` 676 presubmits: 677 org/repo2: 678 - name: org-repo2-presubmit 679 never_run: true // I'm unknown 680 spec: 681 containers: 682 - image: alpine 683 `, 684 }, 685 expectedErr: true, 686 }, 687 } 688 for i := range testcases { 689 tc := testcases[i] 690 t.Run(tc.name, func(t *testing.T) { 691 // Set up config files 692 root := t.TempDir() 693 694 prowConfigFile := filepath.Join(root, "config.yaml") 695 if err := os.WriteFile(prowConfigFile, []byte(tc.configContent), 0666); err != nil { 696 t.Fatalf("Error writing config.yaml file: %v.", err) 697 } 698 var jobConfigDir string 699 if len(tc.jobConfigContent) > 0 { 700 jobConfigDir = filepath.Join(root, "job-config") 701 if err := os.Mkdir(jobConfigDir, 0777); err != nil { 702 t.Fatalf("Error creating job-config directory: %v.", err) 703 } 704 for file, content := range tc.jobConfigContent { 705 file = filepath.Join(jobConfigDir, file) 706 if err := os.WriteFile(file, []byte(content), 0666); err != nil { 707 t.Fatalf("Error writing %q file: %v.", file, err) 708 } 709 } 710 } 711 // Test validation 712 _, err := config.LoadStrict(prowConfigFile, jobConfigDir, nil, "") 713 if (err != nil) != tc.expectedErr { 714 if tc.expectedErr { 715 t.Error("Expected an error, but did not receive one.") 716 } else { 717 content, _ := os.ReadFile(prowConfigFile) 718 t.Log(string(content)) 719 t.Errorf("Unexpected error: %v.", err) 720 } 721 } 722 }) 723 } 724 } 725 726 func TestValidateStrictBranches(t *testing.T) { 727 trueVal := true 728 falseVal := false 729 testcases := []struct { 730 name string 731 config config.ProwConfig 732 733 errItems []string 734 okItems []string 735 }{ 736 { 737 name: "no conflict: no strict config", 738 config: config.ProwConfig{ 739 Tide: config.Tide{ 740 TideGitHubConfig: config.TideGitHubConfig{ 741 Queries: []config.TideQuery{ 742 { 743 Orgs: []string{"kubernetes"}, 744 }, 745 }, 746 }, 747 }, 748 }, 749 errItems: []string{}, 750 okItems: []string{"kubernetes"}, 751 }, 752 { 753 name: "no conflict: no tide config", 754 config: config.ProwConfig{ 755 BranchProtection: config.BranchProtection{ 756 Orgs: map[string]config.Org{ 757 "kubernetes": { 758 Policy: config.Policy{ 759 Protect: &trueVal, 760 RequiredStatusChecks: &config.ContextPolicy{ 761 Strict: &trueVal, 762 }, 763 }, 764 }, 765 }, 766 }, 767 }, 768 errItems: []string{}, 769 okItems: []string{"kubernetes"}, 770 }, 771 { 772 name: "no conflict: tide repo exclusion", 773 config: config.ProwConfig{ 774 Tide: config.Tide{ 775 TideGitHubConfig: config.TideGitHubConfig{ 776 Queries: []config.TideQuery{ 777 { 778 Orgs: []string{"kubernetes"}, 779 ExcludedRepos: []string{"kubernetes/test-infra"}, 780 }, 781 }, 782 }, 783 }, 784 BranchProtection: config.BranchProtection{ 785 Orgs: map[string]config.Org{ 786 "kubernetes": { 787 Policy: config.Policy{ 788 Protect: &falseVal, 789 }, 790 Repos: map[string]config.Repo{ 791 "test-infra": { 792 Policy: config.Policy{ 793 Protect: &trueVal, 794 RequiredStatusChecks: &config.ContextPolicy{ 795 Strict: &trueVal, 796 }, 797 }, 798 }, 799 }, 800 }, 801 }, 802 }, 803 }, 804 errItems: []string{}, 805 okItems: []string{"kubernetes", "kubernetes/test-infra"}, 806 }, 807 { 808 name: "no conflict: protection repo exclusion", 809 config: config.ProwConfig{ 810 Tide: config.Tide{ 811 TideGitHubConfig: config.TideGitHubConfig{ 812 Queries: []config.TideQuery{ 813 { 814 Repos: []string{"kubernetes/test-infra"}, 815 }, 816 }, 817 }, 818 }, 819 BranchProtection: config.BranchProtection{ 820 Orgs: map[string]config.Org{ 821 "kubernetes": { 822 Policy: config.Policy{ 823 Protect: &trueVal, 824 RequiredStatusChecks: &config.ContextPolicy{ 825 Strict: &trueVal, 826 }, 827 }, 828 Repos: map[string]config.Repo{ 829 "test-infra": { 830 Policy: config.Policy{ 831 Protect: &falseVal, 832 }, 833 }, 834 }, 835 }, 836 }, 837 }, 838 }, 839 errItems: []string{}, 840 okItems: []string{"kubernetes", "kubernetes/test-infra"}, 841 }, 842 { 843 name: "conflict: tide more general", 844 config: config.ProwConfig{ 845 Tide: config.Tide{ 846 TideGitHubConfig: config.TideGitHubConfig{ 847 Queries: []config.TideQuery{ 848 { 849 Orgs: []string{"kubernetes"}, 850 }, 851 }, 852 }, 853 }, 854 BranchProtection: config.BranchProtection{ 855 Policy: config.Policy{ 856 Protect: &trueVal, 857 }, 858 Orgs: map[string]config.Org{ 859 "kubernetes": { 860 Repos: map[string]config.Repo{ 861 "test-infra": { 862 Policy: config.Policy{ 863 Protect: &trueVal, 864 RequiredStatusChecks: &config.ContextPolicy{ 865 Strict: &trueVal, 866 }, 867 }, 868 }, 869 }, 870 }, 871 }, 872 }, 873 }, 874 errItems: []string{"kubernetes/test-infra"}, 875 okItems: []string{"kubernetes"}, 876 }, 877 { 878 name: "conflict: tide more specific", 879 config: config.ProwConfig{ 880 Tide: config.Tide{ 881 TideGitHubConfig: config.TideGitHubConfig{ 882 Queries: []config.TideQuery{ 883 { 884 Repos: []string{"kubernetes/test-infra"}, 885 }, 886 }, 887 }, 888 }, 889 BranchProtection: config.BranchProtection{ 890 Policy: config.Policy{ 891 Protect: &trueVal, 892 }, 893 Orgs: map[string]config.Org{ 894 "kubernetes": { 895 Policy: config.Policy{ 896 RequiredStatusChecks: &config.ContextPolicy{ 897 Strict: &trueVal, 898 }, 899 }, 900 }, 901 }, 902 }, 903 }, 904 errItems: []string{"kubernetes/test-infra"}, 905 okItems: []string{"kubernetes"}, 906 }, 907 { 908 name: "conflict: org level", 909 config: config.ProwConfig{ 910 Tide: config.Tide{ 911 TideGitHubConfig: config.TideGitHubConfig{ 912 Queries: []config.TideQuery{ 913 { 914 Orgs: []string{"kubernetes", "k8s"}, 915 }, 916 }, 917 }, 918 }, 919 BranchProtection: config.BranchProtection{ 920 Policy: config.Policy{ 921 Protect: &trueVal, 922 }, 923 Orgs: map[string]config.Org{ 924 "kubernetes": { 925 Policy: config.Policy{ 926 RequiredStatusChecks: &config.ContextPolicy{ 927 Strict: &trueVal, 928 }, 929 }, 930 }, 931 }, 932 }, 933 }, 934 errItems: []string{"kubernetes"}, 935 okItems: []string{}, 936 }, 937 { 938 name: "conflict: repo level", 939 config: config.ProwConfig{ 940 Tide: config.Tide{ 941 TideGitHubConfig: config.TideGitHubConfig{ 942 Queries: []config.TideQuery{ 943 { 944 Repos: []string{"kubernetes/kubernetes"}, 945 }, 946 { 947 Repos: []string{"kubernetes/test-infra"}, 948 }, 949 }, 950 }, 951 }, 952 BranchProtection: config.BranchProtection{ 953 Policy: config.Policy{ 954 Protect: &trueVal, 955 }, 956 Orgs: map[string]config.Org{ 957 "kubernetes": { 958 Repos: map[string]config.Repo{ 959 "kubernetes": { 960 Policy: config.Policy{ 961 RequiredStatusChecks: &config.ContextPolicy{ 962 Strict: &trueVal, 963 }, 964 }, 965 }, 966 }, 967 }, 968 }, 969 }, 970 }, 971 errItems: []string{"kubernetes/kubernetes"}, 972 okItems: []string{"kubernetes", "kubernetes/test-infra"}, 973 }, 974 { 975 name: "conflict: branch level", 976 config: config.ProwConfig{ 977 Tide: config.Tide{ 978 TideGitHubConfig: config.TideGitHubConfig{ 979 Queries: []config.TideQuery{ 980 { 981 Repos: []string{"kubernetes/test-infra"}, 982 IncludedBranches: []string{"master"}, 983 }, 984 { 985 Repos: []string{"kubernetes/kubernetes"}, 986 }, 987 }, 988 }, 989 }, 990 BranchProtection: config.BranchProtection{ 991 Policy: config.Policy{ 992 Protect: &trueVal, 993 }, 994 Orgs: map[string]config.Org{ 995 "kubernetes": { 996 Repos: map[string]config.Repo{ 997 "test-infra": { 998 Branches: map[string]config.Branch{ 999 "master": { 1000 Policy: config.Policy{ 1001 RequiredStatusChecks: &config.ContextPolicy{ 1002 Strict: &trueVal, 1003 }, 1004 }, 1005 }, 1006 }, 1007 }, 1008 }, 1009 }, 1010 }, 1011 }, 1012 }, 1013 errItems: []string{"kubernetes/test-infra"}, 1014 okItems: []string{"kubernetes", "kubernetes/kubernetes"}, 1015 }, 1016 { 1017 name: "conflict: global strict", 1018 config: config.ProwConfig{ 1019 Tide: config.Tide{ 1020 TideGitHubConfig: config.TideGitHubConfig{ 1021 Queries: []config.TideQuery{ 1022 { 1023 Repos: []string{"kubernetes/test-infra"}, 1024 }, 1025 }, 1026 }, 1027 }, 1028 BranchProtection: config.BranchProtection{ 1029 Policy: config.Policy{ 1030 Protect: &trueVal, 1031 RequiredStatusChecks: &config.ContextPolicy{ 1032 Strict: &trueVal, 1033 }, 1034 }, 1035 }, 1036 }, 1037 errItems: []string{"global"}, 1038 okItems: []string{}, 1039 }, 1040 { 1041 name: "no conflict: global strict, Tide disabled", 1042 config: config.ProwConfig{ 1043 BranchProtection: config.BranchProtection{ 1044 Policy: config.Policy{ 1045 Protect: &trueVal, 1046 RequiredStatusChecks: &config.ContextPolicy{ 1047 Strict: &trueVal, 1048 }, 1049 }, 1050 }, 1051 }, 1052 errItems: []string{}, 1053 okItems: []string{"global"}, 1054 }, 1055 } 1056 for i := range testcases { 1057 t.Run(testcases[i].name, func(t *testing.T) { 1058 tc := testcases[i] 1059 t.Parallel() 1060 err := validateStrictBranches(tc.config) 1061 if err == nil && len(tc.errItems) > 0 { 1062 t.Errorf("Expected errors for the following items, but didn't see an error: %v.", tc.errItems) 1063 } else if err != nil && len(tc.errItems) == 0 { 1064 t.Errorf("Unexpected error: %v.", err) 1065 } 1066 if err == nil { 1067 return 1068 } 1069 errText := err.Error() 1070 for _, errItem := range tc.errItems { 1071 // Search for the token while explicitly forbidding neighboring slashes 1072 // so that orgs don't match member repos. 1073 re, err := regexp.Compile(fmt.Sprintf("[^/]%s[^/]", errItem)) 1074 if err != nil { 1075 t.Fatalf("Unexpected error compiling regexp: %v.", err) 1076 } 1077 if !re.MatchString(errText) { 1078 t.Errorf("Error did not reference expected error item %q: %q.", errItem, errText) 1079 } 1080 } 1081 for _, okItem := range tc.okItems { 1082 re, err := regexp.Compile(fmt.Sprintf("[^/]%s[^/]", okItem)) 1083 if err != nil { 1084 t.Fatalf("Unexpected error compiling regexp: %v.", err) 1085 } 1086 if re.MatchString(errText) { 1087 t.Errorf("Error unexpectedly included ok item %q: %q.", okItem, errText) 1088 } 1089 } 1090 }) 1091 } 1092 } 1093 1094 func TestValidateManagedWebhooks(t *testing.T) { 1095 testCases := []struct { 1096 name string 1097 config config.ProwConfig 1098 expectErr bool 1099 }{ 1100 { 1101 name: "empty config", 1102 config: config.ProwConfig{}, 1103 expectErr: false, 1104 }, 1105 { 1106 name: "no duplicate webhooks", 1107 config: config.ProwConfig{ 1108 ManagedWebhooks: config.ManagedWebhooks{ 1109 RespectLegacyGlobalToken: false, 1110 OrgRepoConfig: map[string]config.ManagedWebhookInfo{ 1111 "foo1": {TokenCreatedAfter: time.Now()}, 1112 "foo2": {TokenCreatedAfter: time.Now()}, 1113 "foo/bar": {TokenCreatedAfter: time.Now()}, 1114 "foo/bar1": {TokenCreatedAfter: time.Now()}, 1115 "foo/bar2": {TokenCreatedAfter: time.Now()}, 1116 }, 1117 }, 1118 }, 1119 expectErr: false, 1120 }, 1121 { 1122 name: "has duplicate webhooks", 1123 config: config.ProwConfig{ 1124 ManagedWebhooks: config.ManagedWebhooks{ 1125 OrgRepoConfig: map[string]config.ManagedWebhookInfo{ 1126 "foo": {TokenCreatedAfter: time.Now()}, 1127 "foo1": {TokenCreatedAfter: time.Now()}, 1128 "foo2": {TokenCreatedAfter: time.Now()}, 1129 "foo/bar": {TokenCreatedAfter: time.Now()}, 1130 "foo/bar1": {TokenCreatedAfter: time.Now()}, 1131 "foo/bar2": {TokenCreatedAfter: time.Now()}, 1132 }, 1133 }, 1134 }, 1135 expectErr: true, 1136 }, 1137 { 1138 name: "has multiple duplicate webhooks", 1139 config: config.ProwConfig{ 1140 ManagedWebhooks: config.ManagedWebhooks{ 1141 RespectLegacyGlobalToken: true, 1142 OrgRepoConfig: map[string]config.ManagedWebhookInfo{ 1143 "foo": {TokenCreatedAfter: time.Now()}, 1144 "foo1": {TokenCreatedAfter: time.Now()}, 1145 "foo2": {TokenCreatedAfter: time.Now()}, 1146 "foo/bar": {TokenCreatedAfter: time.Now()}, 1147 "foo/bar1": {TokenCreatedAfter: time.Now()}, 1148 "foo1/bar1": {TokenCreatedAfter: time.Now()}, 1149 }, 1150 }, 1151 }, 1152 expectErr: true, 1153 }, 1154 } 1155 1156 for _, testCase := range testCases { 1157 err := validateManagedWebhooks(&config.Config{ProwConfig: testCase.config}) 1158 if testCase.expectErr && err == nil { 1159 t.Errorf("%s: expected the config %+v to have errors but not", testCase.name, testCase.config) 1160 } 1161 if !testCase.expectErr && err != nil { 1162 t.Errorf("%s: expected the config %+v to be correct but got an error in validation: %v", 1163 testCase.name, testCase.config, err) 1164 } 1165 } 1166 } 1167 1168 func TestWarningEnabled(t *testing.T) { 1169 var testCases = []struct { 1170 name string 1171 warnings []string 1172 excludes []string 1173 candidate string 1174 expected bool 1175 }{ 1176 { 1177 name: "nothing is found in empty sets", 1178 warnings: []string{}, 1179 excludes: []string{}, 1180 candidate: "missing", 1181 expected: false, 1182 }, 1183 { 1184 name: "explicit warning is found", 1185 warnings: []string{"found"}, 1186 excludes: []string{}, 1187 candidate: "found", 1188 expected: true, 1189 }, 1190 { 1191 name: "explicit warning that is excluded is not found", 1192 warnings: []string{"found"}, 1193 excludes: []string{"found"}, 1194 candidate: "found", 1195 expected: false, 1196 }, 1197 } 1198 1199 for _, testCase := range testCases { 1200 opt := options{ 1201 warnings: flagutil.NewStrings(testCase.warnings...), 1202 excludeWarnings: flagutil.NewStrings(testCase.excludes...), 1203 } 1204 if actual, expected := opt.warningEnabled(testCase.candidate), testCase.expected; actual != expected { 1205 t.Errorf("%s: expected warning %s enablement to be %v but got %v", testCase.name, testCase.candidate, expected, actual) 1206 } 1207 } 1208 } 1209 1210 type fakeGHContent map[string]map[string]map[string]bool // org[repo][path] -> exist/does not exist 1211 1212 type fakeGH struct { 1213 files fakeGHContent 1214 archived map[string]bool // org/repo -> true/false 1215 } 1216 1217 func (f fakeGH) GetFile(org, repo, filepath, _ string) ([]byte, error) { 1218 if _, hasOrg := f.files[org]; !hasOrg { 1219 return nil, &github.FileNotFound{} 1220 } 1221 if _, hasRepo := f.files[org][repo]; !hasRepo { 1222 return nil, &github.FileNotFound{} 1223 } 1224 if _, hasPath := f.files[org][repo][filepath]; !hasPath { 1225 return nil, &github.FileNotFound{} 1226 } 1227 1228 return []byte("CONTENT"), nil 1229 } 1230 1231 func (f fakeGH) GetRepos(org string, isUser bool) ([]github.Repo, error) { 1232 if _, hasOrg := f.files[org]; !hasOrg { 1233 return nil, fmt.Errorf("no such org") 1234 } 1235 var repos []github.Repo 1236 for repo := range f.files[org] { 1237 fullname := fmt.Sprintf("%s/%s", org, repo) 1238 _, archived := f.archived[fullname] 1239 repos = append( 1240 repos, 1241 github.Repo{ 1242 Owner: github.User{Login: org}, 1243 Name: repo, 1244 FullName: fullname, 1245 Archived: archived, 1246 }) 1247 } 1248 return repos, nil 1249 } 1250 1251 func TestVerifyOwnersPresence(t *testing.T) { 1252 testCases := []struct { 1253 description string 1254 cfg *plugins.Configuration 1255 gh fakeGH 1256 1257 expected string 1258 }{ 1259 { 1260 description: "org with blunderbuss enabled contains a repo without OWNERS (legacy config)", 1261 cfg: &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"blunderbuss"}})}, 1262 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1263 expected: "the following orgs or repos enable at least one" + 1264 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1265 " its master branch does not contain a root level OWNERS file: [org/repo]", 1266 }, { 1267 description: "org with approve enable contains a repo without OWNERS (legacy config)", 1268 cfg: &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"approve"}})}, 1269 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1270 expected: "the following orgs or repos enable at least one" + 1271 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1272 " its master branch does not contain a root level OWNERS file: [org/repo]", 1273 }, { 1274 description: "org with owners-label enabled contains a repo without OWNERS (legacy config)", 1275 cfg: &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})}, 1276 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1277 expected: "the following orgs or repos enable at least one" + 1278 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1279 " its master branch does not contain a root level OWNERS file: [org/repo]", 1280 }, { 1281 description: "org with owners-label enabled contains an *archived* repo without OWNERS (legacy config)", 1282 cfg: &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})}, 1283 gh: fakeGH{ 1284 files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}, 1285 archived: map[string]bool{"org/repo": true}, 1286 }, 1287 expected: "", 1288 }, { 1289 description: "repo with owners-label enabled does not contain OWNERS (legacy config)", 1290 cfg: &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})}, 1291 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1292 expected: "the following orgs or repos enable at least one" + 1293 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1294 " its master branch does not contain a root level OWNERS file: [org/repo]", 1295 }, { 1296 description: "org with owners-label enabled contains only repos with OWNERS (legacy config)", 1297 cfg: &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})}, 1298 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"OWNERS": true}}}}, 1299 expected: "", 1300 }, { 1301 description: "repo with owners-label enabled contains OWNERS (legacy config)", 1302 cfg: &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})}, 1303 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"OWNERS": true}}}}, 1304 expected: "", 1305 }, { 1306 description: "repo with unrelated plugin enabled does not contain OWNERS (legacy config)", 1307 cfg: &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org/repo": {"cat"}})}, 1308 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1309 expected: "", 1310 }, { 1311 description: "org with blunderbuss enabled contains a repo without OWNERS", 1312 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"blunderbuss"}}}}, 1313 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1314 expected: "the following orgs or repos enable at least one" + 1315 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1316 " its master branch does not contain a root level OWNERS file: [org/repo]", 1317 }, { 1318 description: "org with approve enable contains a repo without OWNERS", 1319 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"approve"}}}}, 1320 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1321 expected: "the following orgs or repos enable at least one" + 1322 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1323 " its master branch does not contain a root level OWNERS file: [org/repo]", 1324 }, { 1325 description: "org with approve excluded contains a repo without OWNERS", 1326 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org": { 1327 Plugins: []string{"approve"}, 1328 ExcludedRepos: []string{"repo"}, 1329 }}}, 1330 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1331 expected: "", 1332 }, { 1333 description: "org with approve repo-enabled contains a repo without OWNERS", 1334 cfg: &plugins.Configuration{Plugins: plugins.Plugins{ 1335 "org": { 1336 Plugins: []string{"approve"}, 1337 ExcludedRepos: []string{"repo"}, 1338 }, 1339 "org/repo": {Plugins: []string{"approve"}}, 1340 }}, 1341 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1342 expected: "the following orgs or repos enable at least one" + 1343 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1344 " its master branch does not contain a root level OWNERS file: [org/repo]", 1345 }, { 1346 description: "org with owners-label enabled contains a repo without OWNERS", 1347 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"owners-label"}}}}, 1348 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1349 expected: "the following orgs or repos enable at least one" + 1350 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1351 " its master branch does not contain a root level OWNERS file: [org/repo]", 1352 }, { 1353 description: "org with owners-label enabled contains an *archived* repo without OWNERS", 1354 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"owners-label"}}}}, 1355 gh: fakeGH{ 1356 files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}, 1357 archived: map[string]bool{"org/repo": true}, 1358 }, 1359 expected: "", 1360 }, { 1361 description: "repo with owners-label enabled does not contain OWNERS", 1362 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org/repo": {Plugins: []string{"owners-label"}}}}, 1363 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1364 expected: "the following orgs or repos enable at least one" + 1365 " plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" + 1366 " its master branch does not contain a root level OWNERS file: [org/repo]", 1367 }, { 1368 description: "org with owners-label enabled contains only repos with OWNERS", 1369 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"owners-label"}}}}, 1370 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"OWNERS": true}}}}, 1371 expected: "", 1372 }, { 1373 description: "repo with owners-label enabled contains OWNERS", 1374 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org/repo": {Plugins: []string{"owners-label"}}}}, 1375 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"OWNERS": true}}}}, 1376 expected: "", 1377 }, { 1378 description: "repo with unrelated plugin enabled does not contain OWNERS", 1379 cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org/repo": {Plugins: []string{"cat"}}}}, 1380 gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}}, 1381 expected: "", 1382 }, 1383 } 1384 1385 for _, tc := range testCases { 1386 t.Run(tc.description, func(t *testing.T) { 1387 var errMessage string 1388 if err := verifyOwnersPresence(tc.cfg, tc.gh); err != nil { 1389 errMessage = err.Error() 1390 } 1391 if errMessage != tc.expected { 1392 t.Errorf("result differs:\n%s", diff.StringDiff(tc.expected, errMessage)) 1393 } 1394 }) 1395 } 1396 } 1397 1398 func TestOptions(t *testing.T) { 1399 1400 var defaultGitHubOptions flagutil.GitHubOptions 1401 defaultGitHubOptions.AddCustomizedFlags(flag.NewFlagSet("", flag.ContinueOnError), throttlerDefaults) 1402 defaultGitHubOptions.AllowAnonymous = true 1403 1404 StringsFlag := func(vals []string) flagutil.Strings { 1405 var flag flagutil.Strings 1406 for _, val := range vals { 1407 flag.Set(val) 1408 } 1409 return flag 1410 } 1411 1412 testCases := []struct { 1413 name string 1414 args []string 1415 expectedOptions *options 1416 expectedError bool 1417 }{ 1418 { 1419 name: "cannot parse argument, reject", 1420 args: []string{ 1421 "--config-path=prow/config.yaml", 1422 "--strict=non-boolean-string", 1423 }, 1424 expectedOptions: nil, 1425 expectedError: true, 1426 }, 1427 { 1428 name: "forgot config-path, reject", 1429 args: []string{"--job-config-path=config/jobs/org/job.yaml"}, 1430 expectedOptions: nil, 1431 expectedError: true, 1432 }, 1433 { 1434 name: "config-path with two warnings but one unknown, reject", 1435 args: []string{ 1436 "--config-path=prow/config.yaml", 1437 "--warnings=mismatched-tide", 1438 "--warnings=unknown-warning", 1439 }, 1440 expectedOptions: nil, 1441 expectedError: true, 1442 }, 1443 { 1444 name: "config-path with many valid options", 1445 args: []string{ 1446 "--config-path=prow/config.yaml", 1447 "--plugin-config=prow/plugins/plugin.yaml", 1448 "--job-config-path=config/jobs/org/job.yaml", 1449 "--warnings=mismatched-tide", 1450 "--warnings=mismatched-tide-lenient", 1451 "--exclude-warning=tide-strict-branch", 1452 "--exclude-warning=mismatched-tide", 1453 "--exclude-warning=ok-if-unknown-warning", 1454 "--strict=true", 1455 "--expensive-checks=false", 1456 }, 1457 expectedOptions: &options{ 1458 config: configflagutil.ConfigOptions{ 1459 ConfigPathFlagName: "config-path", 1460 JobConfigPathFlagName: "job-config-path", 1461 ConfigPath: "prow/config.yaml", 1462 JobConfigPath: "config/jobs/org/job.yaml", 1463 SupplementalProwConfigsFileNameSuffix: "_prowconfig.yaml", 1464 InRepoConfigCacheSize: 200, 1465 }, 1466 pluginsConfig: pluginsflagutil.PluginOptions{ 1467 PluginConfigPath: "prow/plugins/plugin.yaml", 1468 SupplementalPluginsConfigsFileNameSuffix: "_pluginconfig.yaml", 1469 CheckUnknownPlugins: true, 1470 }, 1471 warnings: StringsFlag([]string{"mismatched-tide", "mismatched-tide-lenient"}), 1472 excludeWarnings: StringsFlag([]string{"tide-strict-branch", "mismatched-tide", "ok-if-unknown-warning"}), 1473 strict: true, 1474 expensive: false, 1475 github: defaultGitHubOptions, 1476 }, 1477 expectedError: false, 1478 }, 1479 { 1480 name: "prow-yaml-path without prow-yaml-repo-name is invalid", 1481 args: []string{ 1482 "--prow-yaml-path=my-file", 1483 }, 1484 expectedError: true, 1485 }, 1486 } 1487 1488 for _, tc := range testCases { 1489 t.Run(tc.name, func(t *testing.T) { 1490 flags := flag.NewFlagSet(tc.name, flag.ContinueOnError) 1491 var actualOptions options 1492 switch actualErr := actualOptions.gatherOptions(flags, tc.args); { 1493 case tc.expectedError: 1494 if actualErr == nil { 1495 t.Error("failed to receive an error") 1496 } 1497 case actualErr != nil: 1498 t.Errorf("unexpected error: %v", actualErr) 1499 case !reflect.DeepEqual(&actualOptions, tc.expectedOptions): 1500 t.Errorf("actual differs from expected: %s", cmp.Diff(actualOptions, *tc.expectedOptions, cmp.Exporter(func(_ reflect.Type) bool { return true }))) 1501 } 1502 }) 1503 } 1504 } 1505 1506 func TestValidateJobExtraRefs(t *testing.T) { 1507 testCases := []struct { 1508 name string 1509 extraRefs []prowapi.Refs 1510 expected error 1511 }{ 1512 { 1513 name: "validation error if extra ref specifies the repo for which the job is configured", 1514 extraRefs: []prowapi.Refs{ 1515 { 1516 Org: "org", 1517 Repo: "repo", 1518 }, 1519 }, 1520 expected: fmt.Errorf("invalid job test on repo org/repo: the following refs specified more than once: %s", 1521 "org/repo"), 1522 }, 1523 { 1524 name: "no errors if there are no duplications", 1525 extraRefs: []prowapi.Refs{ 1526 { 1527 Org: "foo", 1528 Repo: "bar", 1529 }, 1530 }, 1531 }, 1532 } 1533 1534 for _, tc := range testCases { 1535 t.Run(tc.name, func(t *testing.T) { 1536 config := config.JobConfig{ 1537 PresubmitsStatic: map[string][]config.Presubmit{ 1538 "org/repo": { 1539 { 1540 JobBase: config.JobBase{ 1541 Name: "test", 1542 UtilityConfig: config.UtilityConfig{ 1543 ExtraRefs: tc.extraRefs, 1544 }, 1545 }, 1546 }, 1547 }, 1548 }, 1549 } 1550 if err := validateJobExtraRefs(config); !reflect.DeepEqual(err, utilerrors.NewAggregate([]error{tc.expected})) { 1551 t.Errorf("%s: did not get expected validation error:\n%v", tc.name, 1552 cmp.Diff(tc.expected, err)) 1553 } 1554 }) 1555 } 1556 } 1557 1558 func TestValidateInRepoConfig(t *testing.T) { 1559 testCases := []struct { 1560 name string 1561 prowYAMLData []byte 1562 strict bool 1563 expectedErr string 1564 }{ 1565 { 1566 name: "Valid prowYAML, no err", 1567 prowYAMLData: []byte(`presubmits: [{"name": "hans", "spec": {"containers": [{}]}}]`), 1568 }, 1569 { 1570 name: "Invalid prowYAML presubmit, err", 1571 prowYAMLData: []byte(`presubmits: [{"name": "hans"}]`), 1572 expectedErr: "failed to validate Prow YAML: invalid presubmit job hans: kubernetes jobs require a spec", 1573 }, 1574 { 1575 name: "Invalid prowYAML postsubmit, err", 1576 prowYAMLData: []byte(`postsubmits: [{"name": "hans"}]`), 1577 expectedErr: "failed to validate Prow YAML: invalid postsubmit job hans: kubernetes jobs require a spec", 1578 }, 1579 { 1580 name: "Absent prowYAML, no err", 1581 }, 1582 { 1583 name: "unknown field prowYAML fails strict validation", 1584 strict: true, 1585 prowYAMLData: []byte(`presubmits: [{"name": "hans", "never_run": "true", "spec": {"containers": [{}]}}]`), 1586 expectedErr: "error unmarshaling JSON: while decoding JSON: json: unknown field \"never_run\"", 1587 }, 1588 } 1589 1590 for _, tc := range testCases { 1591 prowYAMLFileName := "/this-must-not-exist" 1592 1593 if tc.prowYAMLData != nil { 1594 fileName := filepath.Join(t.TempDir(), ".prow.yaml") 1595 if err := os.WriteFile(fileName, tc.prowYAMLData, 0666); err != nil { 1596 t.Fatalf("failed to write to tempfile: %v", err) 1597 } 1598 1599 prowYAMLFileName = fileName 1600 } 1601 1602 // Need an empty file to load the config from so we go through its defaulting 1603 tempConfig, err := os.CreateTemp("", "prow-test") 1604 if err != nil { 1605 t.Fatalf("failed to get tempfile: %v", err) 1606 } 1607 defer func() { 1608 if err := os.Remove(tempConfig.Name()); err != nil { 1609 t.Errorf("failed to remove tempfile: %v", err) 1610 } 1611 }() 1612 if err := tempConfig.Close(); err != nil { 1613 t.Errorf("failed to close tempFile: %v", err) 1614 } 1615 1616 cfg, err := config.Load(tempConfig.Name(), "", nil, "") 1617 if err != nil { 1618 t.Fatalf("failed to load config: %v", err) 1619 } 1620 err = validateInRepoConfig(cfg, prowYAMLFileName, "my/repo", tc.strict) 1621 var errString string 1622 if err != nil { 1623 errString = err.Error() 1624 } 1625 1626 if errString != tc.expectedErr && !strings.Contains(errString, tc.expectedErr) { 1627 t.Errorf("expected error %q does not match actual error %q", tc.expectedErr, errString) 1628 } 1629 } 1630 } 1631 1632 func TestValidateTideContextPolicy(t *testing.T) { 1633 cfg := func(m ...func(*config.Config)) *config.Config { 1634 cfg := &config.Config{} 1635 cfg.PresubmitsStatic = map[string][]config.Presubmit{} 1636 for _, mod := range m { 1637 mod(cfg) 1638 } 1639 return cfg 1640 } 1641 1642 testCases := []struct { 1643 name string 1644 cfg *config.Config 1645 expectedError string 1646 }{ 1647 { 1648 name: "overlapping branch config, error", 1649 cfg: cfg(func(c *config.Config) { 1650 c.PresubmitsStatic["a/b"] = []config.Presubmit{ 1651 {Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"a"}}}, 1652 {AlwaysRun: true, Reporter: config.Reporter{Context: "a"}}, 1653 } 1654 }), 1655 expectedError: "context policy for a branch in a/b is invalid: contexts a are defined as required and required if present", 1656 }, 1657 { 1658 name: "overlapping branch config with empty branch configs, error", 1659 cfg: cfg(func(c *config.Config) { 1660 c.PresubmitsStatic["a/b"] = []config.Presubmit{ 1661 {Reporter: config.Reporter{Context: "a"}}, 1662 {AlwaysRun: true, Reporter: config.Reporter{Context: "a"}}, 1663 } 1664 }), 1665 expectedError: "context policy for master branch in a/b is invalid: contexts a are defined as required and required if present", 1666 }, 1667 { 1668 name: "overlapping branch config, inrepoconfig enabled, error", 1669 cfg: cfg(func(c *config.Config) { 1670 c.InRepoConfig.Enabled = map[string]*bool{"*": utilpointer.Bool(true)} 1671 c.PresubmitsStatic["a/b"] = []config.Presubmit{ 1672 {Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"a"}}}, 1673 {AlwaysRun: true, Reporter: config.Reporter{Context: "a"}}, 1674 } 1675 }), 1676 expectedError: "context policy for a branch in a/b is invalid: contexts a are defined as required and required if present", 1677 }, 1678 { 1679 name: "no overlapping branch config, no error", 1680 cfg: cfg(func(c *config.Config) { 1681 c.PresubmitsStatic["a/b"] = []config.Presubmit{ 1682 {Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"a"}}}, 1683 {AlwaysRun: true, Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"b"}}}, 1684 } 1685 }), 1686 }, 1687 { 1688 name: "repo key is not in org/repo format, no error", 1689 cfg: cfg(func(c *config.Config) { 1690 c.PresubmitsStatic["https://kunit-review.googlesource.com/linux"] = []config.Presubmit{ 1691 {Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"a"}}}, 1692 {AlwaysRun: true, Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"b"}}}, 1693 } 1694 }), 1695 }, 1696 } 1697 1698 for _, tc := range testCases { 1699 t.Run(tc.name, func(t *testing.T) { 1700 // Needed so regexes get compiled 1701 tc.cfg.SetPresubmits(tc.cfg.PresubmitsStatic) 1702 1703 errMsg := "" 1704 if err := validateTideContextPolicy(tc.cfg); err != nil { 1705 errMsg = err.Error() 1706 } 1707 if errMsg != tc.expectedError { 1708 t.Errorf("expected error %q, got error %q", tc.expectedError, errMsg) 1709 } 1710 }) 1711 } 1712 } 1713 1714 func TestValidate(t *testing.T) { 1715 testCases := []struct { 1716 name string 1717 opts options 1718 }{ 1719 { 1720 name: "combined config", 1721 opts: options{ 1722 config: configflagutil.ConfigOptions{ConfigPath: "testdata/combined.yaml"}, 1723 }, 1724 }, 1725 } 1726 1727 for _, tc := range testCases { 1728 t.Run(tc.name, func(t *testing.T) { 1729 if err := validate(tc.opts); err != nil { 1730 t.Fatalf("validation failed: %v", err) 1731 } 1732 }) 1733 } 1734 } 1735 1736 type fakeOpener struct { 1737 io.Opener 1738 content string 1739 readError error 1740 } 1741 1742 func (fo *fakeOpener) Reader(ctx context.Context, path string) (io.ReadCloser, error) { 1743 if fo.readError != nil { 1744 return nil, fo.readError 1745 } 1746 return stdio.NopCloser(strings.NewReader(fo.content)), nil 1747 } 1748 1749 func (fo *fakeOpener) Close() error { 1750 return nil 1751 } 1752 1753 func TestValidateClusterField(t *testing.T) { 1754 testCases := []struct { 1755 name string 1756 cfg *config.Config 1757 clusterStatusFile string 1758 readError error 1759 expectedError string 1760 }{ 1761 { 1762 name: "Jenkins job with unset cluster", 1763 cfg: &config.Config{ 1764 JobConfig: config.JobConfig{ 1765 PresubmitsStatic: map[string][]config.Presubmit{ 1766 "org1/repo1": { 1767 { 1768 JobBase: config.JobBase{ 1769 Agent: "jenkins", 1770 }, 1771 }}}}}, 1772 }, 1773 { 1774 name: "jenkins job with defaulted cluster", 1775 cfg: &config.Config{ 1776 JobConfig: config.JobConfig{ 1777 PresubmitsStatic: map[string][]config.Presubmit{ 1778 "org1/repo1": { 1779 { 1780 JobBase: config.JobBase{ 1781 Agent: "jenkins", 1782 Cluster: "default", 1783 Name: "some-job", 1784 }, 1785 }}}}}, 1786 }, 1787 { 1788 name: "jenkins job must not set cluster", 1789 cfg: &config.Config{ 1790 JobConfig: config.JobConfig{ 1791 PresubmitsStatic: map[string][]config.Presubmit{ 1792 "org1/repo1": { 1793 { 1794 JobBase: config.JobBase{ 1795 Agent: "jenkins", 1796 Cluster: "build1", 1797 Name: "some-job", 1798 }, 1799 }}}}}, 1800 expectedError: "org1/repo1: some-job: cannot set cluster field if agent is jenkins", 1801 }, 1802 { 1803 name: "k8s job can set cluster", 1804 cfg: &config.Config{ 1805 JobConfig: config.JobConfig{ 1806 PresubmitsStatic: map[string][]config.Presubmit{ 1807 "org1/repo1": { 1808 { 1809 JobBase: config.JobBase{ 1810 Agent: "kubernetes", 1811 Cluster: "default", 1812 }, 1813 }}}}}, 1814 }, 1815 { 1816 name: "empty agent job can set cluster", 1817 cfg: &config.Config{ 1818 JobConfig: config.JobConfig{ 1819 PresubmitsStatic: map[string][]config.Presubmit{ 1820 "org1/repo1": { 1821 { 1822 JobBase: config.JobBase{ 1823 Cluster: "default", 1824 }, 1825 }}}}}, 1826 }, 1827 { 1828 name: "cluster validates with lone reachable default cluster", 1829 cfg: &config.Config{ 1830 ProwConfig: config.ProwConfig{ 1831 Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"}, 1832 }, 1833 JobConfig: config.JobConfig{ 1834 PresubmitsStatic: map[string][]config.Presubmit{ 1835 "org1/repo1": { 1836 { 1837 JobBase: config.JobBase{ 1838 Cluster: "default", 1839 }, 1840 }}}}}, 1841 clusterStatusFile: fmt.Sprintf(`{"default": %q}`, plank.ClusterStatusReachable), 1842 }, 1843 { 1844 name: "cluster validates with multiple clusters, specified is reachable", 1845 cfg: &config.Config{ 1846 ProwConfig: config.ProwConfig{ 1847 Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"}, 1848 }, 1849 JobConfig: config.JobConfig{ 1850 PresubmitsStatic: map[string][]config.Presubmit{ 1851 "org1/repo1": { 1852 { 1853 JobBase: config.JobBase{ 1854 Cluster: "build1", 1855 }, 1856 }}}}}, 1857 clusterStatusFile: fmt.Sprintf(`{"default": %q, "build1": %q, "build2": %q}`, plank.ClusterStatusReachable, plank.ClusterStatusReachable, plank.ClusterStatusError), 1858 }, 1859 { 1860 name: "cluster validates with multiple clusters, specified is unreachable (just warn)", 1861 cfg: &config.Config{ 1862 ProwConfig: config.ProwConfig{ 1863 Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"}, 1864 }, 1865 JobConfig: config.JobConfig{ 1866 PresubmitsStatic: map[string][]config.Presubmit{ 1867 "org1/repo1": { 1868 { 1869 JobBase: config.JobBase{ 1870 Name: "my-job", 1871 Cluster: "build2", 1872 }, 1873 }}}}}, 1874 clusterStatusFile: fmt.Sprintf(`{"default": %q, "build1": %q, "build2": %q}`, plank.ClusterStatusReachable, plank.ClusterStatusReachable, plank.ClusterStatusError), 1875 }, 1876 { 1877 name: "cluster fails validation with multiple clusters, specified is unrecognized", 1878 cfg: &config.Config{ 1879 ProwConfig: config.ProwConfig{ 1880 Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"}, 1881 }, 1882 JobConfig: config.JobConfig{ 1883 PresubmitsStatic: map[string][]config.Presubmit{ 1884 "org1/repo1": { 1885 { 1886 JobBase: config.JobBase{ 1887 Name: "my-job", 1888 Cluster: "build3", 1889 }, 1890 }}}}}, 1891 clusterStatusFile: fmt.Sprintf(`{"default": %q, "build1": %q, "build2": %q}`, plank.ClusterStatusReachable, plank.ClusterStatusReachable, plank.ClusterStatusError), 1892 expectedError: "org1/repo1: job configuration for \"my-job\" specifies unknown 'cluster' value \"build3\"", 1893 }, 1894 { 1895 name: "cluster validation skipped if status file does not exist yet", 1896 cfg: &config.Config{ 1897 ProwConfig: config.ProwConfig{ 1898 Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"}, 1899 }, 1900 JobConfig: config.JobConfig{ 1901 PresubmitsStatic: map[string][]config.Presubmit{ 1902 "org1/repo1": { 1903 { 1904 JobBase: config.JobBase{ 1905 Cluster: "build1", 1906 }, 1907 }}}}}, 1908 readError: os.ErrNotExist, 1909 }, 1910 } 1911 1912 for i := range testCases { 1913 tc := testCases[i] 1914 t.Run(tc.name, func(t *testing.T) { 1915 t.Parallel() 1916 opener := fakeOpener{content: tc.clusterStatusFile, readError: tc.readError} 1917 errMsg := "" 1918 if err := validateCluster(tc.cfg, &opener); err != nil { 1919 errMsg = err.Error() 1920 } 1921 if errMsg != tc.expectedError { 1922 t.Errorf("expected error %q, got error %q", tc.expectedError, errMsg) 1923 } 1924 }) 1925 } 1926 } 1927 1928 func TestValidateAdditionalProwConfigIsInOrgRepoDirectoryStructure(t *testing.T) { 1929 t.Parallel() 1930 const root = "root" 1931 const invalidConfig = `[]` 1932 const validGlobalConfig = ` 1933 sinker: 1934 exclude_clusters: 1935 - default 1936 slack_reporter_configs: 1937 '*': 1938 channel: '#general-announcements' 1939 job_states_to_report: 1940 - failure 1941 - error 1942 - success 1943 report_template: Job {{.Spec.Job}} ended with status {{.Status.State}}.` 1944 const validOrgConfig = ` 1945 branch-protection: 1946 orgs: 1947 my-org: 1948 protect: true 1949 tide: 1950 merge_method: 1951 my-org: squash 1952 slack_reporter_configs: 1953 my-org: 1954 channel: '#my-org-announcements' 1955 job_states_to_report: 1956 - failure 1957 - error 1958 report_template: Job {{.Spec.Job}} needs my-org maintainers attention.` 1959 const validRepoConfig = ` 1960 branch-protection: 1961 orgs: 1962 my-org: 1963 repos: 1964 my-repo: 1965 protect: true 1966 tide: 1967 merge_method: 1968 my-org/my-repo: squash 1969 slack_reporter_configs: 1970 my-org/my-repo: 1971 channel: '#my-repo-announcements' 1972 job_states_to_report: 1973 - failure 1974 report_template: Job {{.Spec.Job}} needs my-repo maintainers attention.` 1975 const validGlobalPluginsConfig = ` 1976 blunderbuss: 1977 max_request_count: 2 1978 request_count: 2 1979 use_status_availability: true` 1980 const validOrgPluginsConfig = ` 1981 label: 1982 restricted_labels: 1983 my-org: 1984 - label: cherry-pick-approved 1985 allowed_teams: 1986 - patch-managers 1987 plugins: 1988 my-org: 1989 plugins: 1990 - assign` 1991 const validRepoPluginsConfig = ` 1992 plugins: 1993 my-org/my-repo: 1994 plugins: 1995 - assign` 1996 1997 tests := []struct { 1998 name string 1999 fs fstest.MapFS 2000 2001 expectedErrorMessage string 2002 }{ 2003 { 2004 name: "No configs, no error", 2005 fs: testfs(map[string]string{root + "/OWNERS": "some-owners"}), 2006 }, 2007 { 2008 name: "Config directly below root, no error", 2009 fs: testfs(map[string]string{ 2010 root + "/cfg.yaml": validGlobalConfig, 2011 root + "/plugins.yaml": validGlobalPluginsConfig, 2012 }), 2013 }, 2014 { 2015 name: "Valid org config", 2016 fs: testfs(map[string]string{ 2017 root + "/my-org/cfg.yaml": validOrgConfig, 2018 root + "/my-org/plugins.yaml": validOrgPluginsConfig, 2019 }), 2020 }, 2021 { 2022 name: "Valid org config for wrong org", 2023 fs: testfs(map[string]string{ 2024 root + "/my-other-org/cfg.yaml": validOrgConfig, 2025 root + "/my-other-org/plugins.yaml": validOrgPluginsConfig, 2026 }), 2027 expectedErrorMessage: `[config root/my-other-org/cfg.yaml is invalid: Must contain only config for org my-other-org, but contains config for org my-org, config root/my-other-org/plugins.yaml is invalid: Must contain only config for org my-other-org, but contains config for org my-org]`, 2028 }, 2029 { 2030 name: "Invalid org config", 2031 fs: testfs(map[string]string{ 2032 root + "/my-org/cfg.yaml": invalidConfig, 2033 root + "/my-org/plugins.yaml": invalidConfig, 2034 }), 2035 expectedErrorMessage: `[failed to unmarshal root/my-org/cfg.yaml into *config.Config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type config.Config, failed to unmarshal root/my-org/plugins.yaml into *plugins.Configuration: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type plugins.Configuration]`, 2036 }, 2037 { 2038 name: "Repo config at org level", 2039 fs: testfs(map[string]string{ 2040 root + "/my-org/cfg.yaml": validRepoConfig, 2041 root + "/my-org/plugins.yaml": validRepoPluginsConfig, 2042 }), 2043 expectedErrorMessage: `[config root/my-org/cfg.yaml is invalid: Must contain only config for org my-org, but contains config for repo my-org/my-repo, config root/my-org/plugins.yaml is invalid: Must contain only config for org my-org, but contains config for repo my-org/my-repo]`, 2044 }, 2045 { 2046 name: "Valid repo config", 2047 fs: testfs(map[string]string{ 2048 root + "/my-org/my-repo/cfg.yaml": validRepoConfig, 2049 root + "/my-org/my-repo/plugins.yaml": validRepoPluginsConfig, 2050 }), 2051 }, 2052 { 2053 name: "Valid repo config for wrong repo", 2054 fs: testfs(map[string]string{ 2055 root + "/my-org/my-other-repo/cfg.yaml": validRepoConfig, 2056 root + "/my-org/my-other-repo/plugins.yaml": validRepoPluginsConfig, 2057 }), 2058 expectedErrorMessage: `[config root/my-org/my-other-repo/cfg.yaml is invalid: Must only contain config for repo my-org/my-other-repo, but contains config for repo my-org/my-repo, config root/my-org/my-other-repo/plugins.yaml is invalid: Must only contain config for repo my-org/my-other-repo, but contains config for repo my-org/my-repo]`, 2059 }, 2060 { 2061 name: "Invalid repo config", 2062 fs: testfs(map[string]string{ 2063 root + "/my-org/my-repo/cfg.yaml": invalidConfig, 2064 root + "/my-org/my-repo/plugins.yaml": invalidConfig, 2065 }), 2066 expectedErrorMessage: `[failed to unmarshal root/my-org/my-repo/cfg.yaml into *config.Config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type config.Config, failed to unmarshal root/my-org/my-repo/plugins.yaml into *plugins.Configuration: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type plugins.Configuration]`, 2067 }, 2068 { 2069 name: "Org config at repo level", 2070 fs: testfs(map[string]string{ 2071 root + "/my-org/my-repo/cfg.yaml": validOrgConfig, 2072 root + "/my-org/my-repo/plugins.yaml": validOrgPluginsConfig, 2073 }), 2074 expectedErrorMessage: `[config root/my-org/my-repo/cfg.yaml is invalid: Must only contain config for repo my-org/my-repo, but contains config for org my-org, config root/my-org/my-repo/plugins.yaml is invalid: Must only contain config for repo my-org/my-repo, but contains config for org my-org]`, 2075 }, 2076 { 2077 name: "Nested too deeply", 2078 fs: testfs(map[string]string{ 2079 root + "/my-org/my-repo/nest/cfg.yaml": validOrgConfig, 2080 root + "/my-org/my-repo/nest/plugins.yaml": validOrgPluginsConfig, 2081 }), 2082 2083 expectedErrorMessage: `[config root/my-org/my-repo/nest/cfg.yaml is at an invalid location. All configs must be below root. If they are org-specific, they must be in a folder named like the org. If they are repo-specific, they must be in a folder named like the repo below a folder named like the org., config root/my-org/my-repo/nest/plugins.yaml is at an invalid location. All configs must be below root. If they are org-specific, they must be in a folder named like the org. If they are repo-specific, they must be in a folder named like the repo below a folder named like the org.]`, 2084 }, 2085 } 2086 2087 for _, tc := range tests { 2088 t.Run(tc.name, func(t *testing.T) { 2089 var errMsg string 2090 err := validateAdditionalProwConfigIsInOrgRepoDirectoryStructure(tc.fs, []string{root}, []string{root}, "cfg.yaml", "plugins.yaml") 2091 if err != nil { 2092 errMsg = err.Error() 2093 } 2094 if tc.expectedErrorMessage != errMsg { 2095 t.Errorf("expected error %s, got %s", tc.expectedErrorMessage, errMsg) 2096 } 2097 }) 2098 } 2099 } 2100 2101 func testfs(files map[string]string) fstest.MapFS { 2102 filesystem := fstest.MapFS{} 2103 for path, content := range files { 2104 filesystem[path] = &fstest.MapFile{Data: []byte(content)} 2105 } 2106 return filesystem 2107 } 2108 2109 func TestValidateUnmanagedBranchprotectionConfigDoesntHaveSubconfig(t *testing.T) { 2110 t.Parallel() 2111 bpConfigWithSettingsOnAllLayers := func(m ...func(*config.BranchProtection)) config.BranchProtection { 2112 cfg := config.BranchProtection{ 2113 Policy: config.Policy{Exclude: []string{"some-regex"}}, 2114 Orgs: map[string]config.Org{ 2115 "my-org": { 2116 Policy: config.Policy{Exclude: []string{"some-regex"}}, 2117 Repos: map[string]config.Repo{ 2118 "my-repo": { 2119 Policy: config.Policy{Exclude: []string{"some-regex"}}, 2120 Branches: map[string]config.Branch{ 2121 "my-branch": { 2122 Policy: config.Policy{Exclude: []string{"some-regex"}}, 2123 }, 2124 }, 2125 }, 2126 }, 2127 }, 2128 }, 2129 } 2130 2131 for _, modify := range m { 2132 modify(&cfg) 2133 } 2134 2135 return cfg 2136 } 2137 2138 testCases := []struct { 2139 name string 2140 config config.BranchProtection 2141 2142 expectedErrorMsg string 2143 }{ 2144 { 2145 name: "Empty config, no error", 2146 }, 2147 { 2148 name: "Globally disabled, errors for global and org config", 2149 config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) { 2150 bp.Unmanaged = utilpointer.Bool(true) 2151 }), 2152 2153 expectedErrorMsg: `[branch protection is globally set to unmanaged, but has configuration, branch protection config is globally set to unmanaged but has configuration for org my-org without setting the org to unmanaged: false]`, 2154 }, 2155 { 2156 name: "Org-level disabled, errors for org policy and repos", 2157 config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) { 2158 p := bp.Orgs["my-org"] 2159 p.Unmanaged = utilpointer.Bool(true) 2160 bp.Orgs["my-org"] = p 2161 }), 2162 2163 expectedErrorMsg: `[branch protection config for org my-org is set to unmanaged, but it defines settings, branch protection config for repo my-org/my-repo is defined, but branch protection is unmanaged for org my-org without setting the repo to unmanaged: false]`, 2164 }, 2165 2166 { 2167 name: "Repo-level disabled, errors for repo policy and branches", 2168 config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) { 2169 p := bp.Orgs["my-org"].Repos["my-repo"] 2170 p.Unmanaged = utilpointer.Bool(true) 2171 bp.Orgs["my-org"].Repos["my-repo"] = p 2172 }), 2173 2174 expectedErrorMsg: `[branch protection config for repo my-org/my-repo is set to unmanaged, but it defines settings, branch protection for repo my-org/my-repo is set to unmanaged, but it defines settings for branch my-branch without setting the branch to unmanaged: false]`, 2175 }, 2176 2177 { 2178 name: "Branch-level disabled, errors for branch policy", 2179 config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) { 2180 p := bp.Orgs["my-org"].Repos["my-repo"].Branches["my-branch"] 2181 p.Unmanaged = utilpointer.Bool(true) 2182 bp.Orgs["my-org"].Repos["my-repo"].Branches["my-branch"] = p 2183 }), 2184 2185 expectedErrorMsg: `branch protection config for branch my-branch in repo my-org/my-repo is set to unmanaged but defines settings`, 2186 }, 2187 { 2188 name: "unmanaged repo level is overridden by branch level, no errors", 2189 config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) { 2190 repoP := bp.Orgs["my-org"].Repos["my-repo"] 2191 repoP.Unmanaged = utilpointer.Bool(true) 2192 bp.Orgs["my-org"].Repos["my-repo"] = repoP 2193 p := bp.Orgs["my-org"].Repos["my-repo"].Branches["my-branch"] 2194 p.Unmanaged = utilpointer.Bool(false) 2195 bp.Orgs["my-org"].Repos["my-repo"].Branches["my-branch"] = p 2196 }), 2197 }, 2198 } 2199 2200 for _, tc := range testCases { 2201 var errMsg string 2202 err := validateUnmanagedBranchprotectionConfigDoesntHaveSubconfig(tc.config) 2203 if err != nil { 2204 errMsg = err.Error() 2205 } 2206 if tc.expectedErrorMsg != errMsg { 2207 t.Errorf("expected error message\n%s\ngot error message\n%s", tc.expectedErrorMsg, errMsg) 2208 } 2209 } 2210 } 2211 2212 type fakeGhAppListingClient struct { 2213 installations []github.AppInstallation 2214 } 2215 2216 func (f *fakeGhAppListingClient) ListAppInstallations() ([]github.AppInstallation, error) { 2217 return f.installations, nil 2218 } 2219 2220 func TestValidateGitHubAppIsInstalled(t *testing.T) { 2221 t.Parallel() 2222 testCases := []struct { 2223 name string 2224 allRepos sets.Set[string] 2225 installations []github.AppInstallation 2226 2227 expectedErrorMsg string 2228 }{ 2229 { 2230 name: "Installations exist", 2231 allRepos: sets.New[string]("org/repo", "org-a/repo-a", "org-b/repo-b"), 2232 installations: []github.AppInstallation{ 2233 {Account: github.User{Login: "org"}}, 2234 {Account: github.User{Login: "org-a"}}, 2235 {Account: github.User{Login: "org-b"}}, 2236 }, 2237 }, 2238 { 2239 name: "Some installations exist", 2240 allRepos: sets.New[string]("org/repo", "org-a/repo-a", "org-b/repo-b"), 2241 installations: []github.AppInstallation{ 2242 {Account: github.User{Login: "org"}}, 2243 {Account: github.User{Login: "org-a"}}, 2244 }, 2245 2246 expectedErrorMsg: `There is configuration for the GitHub org "org-b" but the GitHub app is not installed there`, 2247 }, 2248 { 2249 name: "No installations exist", 2250 allRepos: sets.New[string]("org/repo", "org-a/repo-a", "org-b/repo-b"), 2251 2252 expectedErrorMsg: `[There is configuration for the GitHub org "org-a" but the GitHub app is not installed there, There is configuration for the GitHub org "org-b" but the GitHub app is not installed there, There is configuration for the GitHub org "org" but the GitHub app is not installed there]`, 2253 }, 2254 } 2255 2256 for _, tc := range testCases { 2257 t.Run(tc.name, func(t *testing.T) { 2258 var actualErrMsg string 2259 if err := validateGitHubAppIsInstalled(&fakeGhAppListingClient{installations: tc.installations}, tc.allRepos); err != nil { 2260 actualErrMsg = err.Error() 2261 } 2262 2263 if actualErrMsg != tc.expectedErrorMsg { 2264 t.Errorf("expected error %q, got error %q", tc.expectedErrorMsg, actualErrMsg) 2265 } 2266 }) 2267 } 2268 } 2269 2270 func TestVerifyLabelPlugin(t *testing.T) { 2271 t.Parallel() 2272 testCases := []struct { 2273 name string 2274 label plugins.Label 2275 expectedErrorMsg string 2276 }{ 2277 { 2278 name: "empty label config is valid", 2279 }, 2280 { 2281 name: "cannot use the empty string as label name", 2282 label: plugins.Label{ 2283 RestrictedLabels: map[string][]plugins.RestrictedLabel{ 2284 "openshift/machine-config-operator": { 2285 { 2286 Label: "", 2287 AllowedTeams: []string{"openshift-patch-managers"}, 2288 }, 2289 { 2290 Label: "backport-risk-assessed", 2291 AllowedUsers: []string{"kikisdeliveryservice", "sinnykumari", "yuqi-zhang"}, 2292 }, 2293 }, 2294 }, 2295 }, 2296 expectedErrorMsg: "the following orgs or repos have configuration of label plugin using the empty string as label name in restricted labels: openshift/machine-config-operator", 2297 }, 2298 { 2299 name: "valid after removing the restricted labels for the empty string", 2300 label: plugins.Label{ 2301 RestrictedLabels: map[string][]plugins.RestrictedLabel{ 2302 "openshift/machine-config-operator": { 2303 { 2304 Label: "backport-risk-assessed", 2305 AllowedUsers: []string{"kikisdeliveryservice", "sinnykumari", "yuqi-zhang"}, 2306 }, 2307 }, 2308 }, 2309 }, 2310 }, 2311 { 2312 name: "two invalid label configs", 2313 label: plugins.Label{ 2314 RestrictedLabels: map[string][]plugins.RestrictedLabel{ 2315 "orgRepo1": { 2316 { 2317 Label: "", 2318 AllowedTeams: []string{"some-team"}, 2319 }, 2320 }, 2321 "orgRepo2": { 2322 { 2323 Label: "", 2324 AllowedUsers: []string{"some-user"}, 2325 }, 2326 }, 2327 }, 2328 }, 2329 expectedErrorMsg: "the following orgs or repos have configuration of label plugin using the empty string as label name in restricted labels: orgRepo1, orgRepo2", 2330 }, 2331 { 2332 name: "invalid when additional and restricted labels are the same", 2333 label: plugins.Label{ 2334 AdditionalLabels: []string{"cherry-pick-approved"}, 2335 RestrictedLabels: map[string][]plugins.RestrictedLabel{ 2336 "orgRepo1": { 2337 { 2338 Label: "cherry-pick-approved", 2339 }, 2340 }, 2341 }, 2342 }, 2343 expectedErrorMsg: "the following orgs or repos have configuration of label plugin using the restricted label cherry-pick-approved which is also configured as an additional label: orgRepo1", 2344 }, 2345 { 2346 name: "invalid when additional and restricted labels are the same in multiple orgRepos and empty string", 2347 label: plugins.Label{ 2348 AdditionalLabels: []string{"cherry-pick-approved"}, 2349 RestrictedLabels: map[string][]plugins.RestrictedLabel{ 2350 "orgRepo1": { 2351 { 2352 Label: "cherry-pick-approved", 2353 }, 2354 }, 2355 "orgRepo2": { 2356 { 2357 Label: "", 2358 }, 2359 }, 2360 "orgRepo3": { 2361 { 2362 Label: "cherry-pick-approved", 2363 }, 2364 }, 2365 }, 2366 }, 2367 expectedErrorMsg: "[the following orgs or repos have configuration of label plugin using the restricted label cherry-pick-approved which is also configured as an additional label: orgRepo1, orgRepo3, " + 2368 "the following orgs or repos have configuration of label plugin using the empty string as label name in restricted labels: orgRepo2]", 2369 }, 2370 } 2371 2372 for _, tc := range testCases { 2373 t.Run(tc.name, func(t *testing.T) { 2374 var actualErrMsg string 2375 if err := verifyLabelPlugin(tc.label); err != nil { 2376 actualErrMsg = err.Error() 2377 } 2378 if actualErrMsg != tc.expectedErrorMsg { 2379 t.Errorf("expected error %q, got error %q", tc.expectedErrorMsg, actualErrMsg) 2380 } 2381 }) 2382 } 2383 } 2384 2385 func TestValidateRequiredJobAnnotations(t *testing.T) { 2386 tc := []struct { 2387 name string 2388 presubmits []config.Presubmit 2389 postsubmits []config.Postsubmit 2390 periodics []config.Periodic 2391 expectedErr bool 2392 expectedAnnotations []string 2393 }{ 2394 { 2395 name: "no annotation is required, pass", 2396 presubmits: []config.Presubmit{ 2397 { 2398 JobBase: config.JobBase{}, 2399 }, 2400 }, 2401 postsubmits: []config.Postsubmit{ 2402 { 2403 JobBase: config.JobBase{ 2404 Annotations: map[string]string{"prow.k8s.io/cat": "meow"}, 2405 }, 2406 }, 2407 }, 2408 periodics: []config.Periodic{ 2409 { 2410 JobBase: config.JobBase{}, 2411 }, 2412 }, 2413 expectedErr: false, 2414 expectedAnnotations: nil, 2415 }, 2416 { 2417 name: "jobs don't have required annotation, fail", 2418 presubmits: []config.Presubmit{ 2419 { 2420 JobBase: config.JobBase{}, 2421 }, 2422 }, 2423 postsubmits: []config.Postsubmit{ 2424 { 2425 JobBase: config.JobBase{ 2426 Annotations: map[string]string{"prow.k8s.io/cat": "meow"}, 2427 }, 2428 }, 2429 }, 2430 periodics: []config.Periodic{ 2431 { 2432 JobBase: config.JobBase{}, 2433 }, 2434 }, 2435 expectedAnnotations: []string{"prow.k8s.io/maintainer"}, 2436 expectedErr: true, 2437 }, 2438 { 2439 name: "jobs have required annotations, pass", 2440 presubmits: []config.Presubmit{ 2441 { 2442 JobBase: config.JobBase{ 2443 Annotations: map[string]string{"prow.k8s.io/maintainer": "job-maintainer"}, 2444 }, 2445 }, 2446 }, 2447 postsubmits: []config.Postsubmit{ 2448 { 2449 JobBase: config.JobBase{ 2450 Annotations: map[string]string{"prow.k8s.io/maintainer": "job-maintainer"}, 2451 }, 2452 }, 2453 }, 2454 periodics: []config.Periodic{ 2455 { 2456 JobBase: config.JobBase{ 2457 Annotations: map[string]string{"prow.k8s.io/maintainer": "job-maintainer"}, 2458 }, 2459 }, 2460 }, 2461 expectedAnnotations: []string{"prow.k8s.io/maintainer"}, 2462 expectedErr: false, 2463 }, 2464 } 2465 2466 for _, c := range tc { 2467 t.Run(c.name, func(t *testing.T) { 2468 jcfg := config.JobConfig{ 2469 PresubmitsStatic: map[string][]config.Presubmit{"org/repo": c.presubmits}, 2470 PostsubmitsStatic: map[string][]config.Postsubmit{"org/repo": c.postsubmits}, 2471 Periodics: c.periodics, 2472 } 2473 err := validateRequiredJobAnnotations(c.expectedAnnotations, jcfg) 2474 if c.expectedErr && err == nil { 2475 t.Errorf("Expected error but got none") 2476 } 2477 if !c.expectedErr && err != nil { 2478 t.Errorf("Got error but didn't expect one: %v", err) 2479 } 2480 }) 2481 } 2482 }