k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/releng/config-forker/main.go (about) 1 /* 2 Copyright 2019 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 "bytes" 21 "errors" 22 "flag" 23 "fmt" 24 "log" 25 "os" 26 "regexp" 27 "strings" 28 "text/template" 29 30 gyaml "gopkg.in/yaml.v2" 31 "sigs.k8s.io/yaml" 32 33 v1 "k8s.io/api/core/v1" 34 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 35 "sigs.k8s.io/prow/pkg/config" 36 ) 37 38 const ( 39 forkAnnotation = "fork-per-release" 40 suffixAnnotation = "fork-per-release-generic-suffix" 41 periodicIntervalAnnotation = "fork-per-release-periodic-interval" 42 cronAnnotation = "fork-per-release-cron" 43 replacementAnnotation = "fork-per-release-replacements" 44 deletionAnnotation = "fork-per-release-deletions" 45 testgridDashboardsAnnotation = "testgrid-dashboards" 46 testgridTabNameAnnotation = "testgrid-tab-name" 47 descriptionAnnotation = "description" 48 ) 49 50 func generatePostsubmits(c config.JobConfig, vars templateVars) (map[string][]config.Postsubmit, error) { 51 newPostsubmits := map[string][]config.Postsubmit{} 52 for repo, postsubmits := range c.PostsubmitsStatic { 53 for _, postsubmit := range postsubmits { 54 if postsubmit.Annotations[forkAnnotation] != "true" { 55 continue 56 } 57 p := postsubmit 58 p.Name = generateNameVariant(p.Name, vars.Version, postsubmit.Annotations[suffixAnnotation] == "true") 59 p.SkipBranches = nil 60 p.Branches = []string{"release-" + vars.Version} 61 if p.Spec != nil { 62 for i := range p.Spec.Containers { 63 c := &p.Spec.Containers[i] 64 c.Env = fixEnvVars(c.Env, vars.Version) 65 c.Image = fixImage(c.Image, vars.Version) 66 var err error 67 c.Command, err = performReplacement(c.Command, vars, p.Annotations[replacementAnnotation]) 68 if err != nil { 69 return nil, fmt.Errorf("%s: %w", postsubmit.Name, err) 70 } 71 c.Args, err = performReplacement(c.Args, vars, p.Annotations[replacementAnnotation]) 72 if err != nil { 73 return nil, fmt.Errorf("%s: %w", postsubmit.Name, err) 74 } 75 for i := range c.Env { 76 c.Env[i].Name, c.Env[i].Value, err = performEnvReplacement(c.Env[i].Name, c.Env[i].Value, vars, p.Annotations[replacementAnnotation]) 77 if err != nil { 78 return nil, fmt.Errorf("%s: %w", postsubmit.Name, err) 79 } 80 } 81 } 82 } 83 p.Annotations = cleanAnnotations(fixTestgridAnnotations(p.Annotations, vars.Version, false)) 84 newPostsubmits[repo] = append(newPostsubmits[repo], p) 85 } 86 } 87 return newPostsubmits, nil 88 } 89 90 func generatePresubmits(c config.JobConfig, vars templateVars) (map[string][]config.Presubmit, error) { 91 newPresubmits := map[string][]config.Presubmit{} 92 for repo, presubmits := range c.PresubmitsStatic { 93 for _, presubmit := range presubmits { 94 if presubmit.Annotations[forkAnnotation] != "true" { 95 continue 96 } 97 p := presubmit 98 p.SkipBranches = nil 99 p.Branches = []string{"release-" + vars.Version} 100 p.Context = generatePresubmitContextVariant(p.Name, p.Context, vars.Version) 101 if p.Spec != nil { 102 for i := range p.Spec.Containers { 103 c := &p.Spec.Containers[i] 104 c.Env = fixEnvVars(c.Env, vars.Version) 105 c.Image = fixImage(c.Image, vars.Version) 106 var err error 107 c.Command, err = performReplacement(c.Command, vars, p.Annotations[replacementAnnotation]) 108 if err != nil { 109 return nil, fmt.Errorf("%s: %w", presubmit.Name, err) 110 } 111 c.Args, err = performReplacement(c.Args, vars, p.Annotations[replacementAnnotation]) 112 if err != nil { 113 return nil, fmt.Errorf("%s: %w", presubmit.Name, err) 114 } 115 for i := range c.Env { 116 c.Env[i].Name, c.Env[i].Value, err = performEnvReplacement(c.Env[i].Name, c.Env[i].Value, vars, p.Annotations[replacementAnnotation]) 117 if err != nil { 118 return nil, fmt.Errorf("%s: %w", presubmit.Name, err) 119 } 120 } 121 } 122 } 123 p.Annotations = cleanAnnotations(fixTestgridAnnotations(p.Annotations, vars.Version, true)) 124 newPresubmits[repo] = append(newPresubmits[repo], p) 125 } 126 } 127 return newPresubmits, nil 128 } 129 130 func shouldDecorate(c *config.JobConfig, util config.UtilityConfig) bool { 131 if util.Decorate != nil { 132 return *util.Decorate 133 } 134 return c.DecorateAllJobs 135 } 136 137 func generatePeriodics(conf config.JobConfig, vars templateVars) ([]config.Periodic, error) { 138 var newPeriodics []config.Periodic 139 for _, periodic := range conf.Periodics { 140 if periodic.Annotations[forkAnnotation] != "true" { 141 continue 142 } 143 p := periodic 144 p.Name = generateNameVariant(p.Name, vars.Version, periodic.Annotations[suffixAnnotation] == "true") 145 if p.Spec != nil { 146 for i := range p.Spec.Containers { 147 c := &p.Spec.Containers[i] 148 c.Image = fixImage(c.Image, vars.Version) 149 c.Env = fixEnvVars(c.Env, vars.Version) 150 if !shouldDecorate(&conf, p.JobBase.UtilityConfig) { 151 c.Command = fixBootstrapArgs(c.Command, vars.Version) 152 c.Args = fixBootstrapArgs(c.Args, vars.Version) 153 } 154 var err error 155 c.Command, err = performReplacement(c.Command, vars, p.Annotations[replacementAnnotation]) 156 if err != nil { 157 return nil, fmt.Errorf("%s: %w", periodic.Name, err) 158 } 159 c.Args, err = performReplacement(c.Args, vars, p.Annotations[replacementAnnotation]) 160 if err != nil { 161 return nil, fmt.Errorf("%s: %w", periodic.Name, err) 162 } 163 for i := range c.Env { 164 c.Env[i].Name, c.Env[i].Value, err = performEnvReplacement(c.Env[i].Name, c.Env[i].Value, vars, p.Annotations[replacementAnnotation]) 165 if err != nil { 166 return nil, fmt.Errorf("%s: %w", periodic.Name, err) 167 } 168 } 169 } 170 } 171 if shouldDecorate(&conf, p.JobBase.UtilityConfig) { 172 p.ExtraRefs = fixExtraRefs(p.ExtraRefs, vars.Version) 173 } 174 if interval, ok := p.Annotations[periodicIntervalAnnotation]; ok { 175 if _, ok := p.Annotations[cronAnnotation]; ok { 176 return nil, fmt.Errorf("%q specifies both %s and %s, which is illegal", periodic.Name, periodicIntervalAnnotation, cronAnnotation) 177 } 178 f := strings.Fields(interval) 179 if len(f) > 0 { 180 p.Interval = f[0] 181 p.Cron = "" 182 p.Annotations[periodicIntervalAnnotation] = strings.Join(f[1:], " ") 183 } 184 } 185 if cron, ok := p.Annotations[cronAnnotation]; ok { 186 c := strings.Split(cron, ", ") 187 if len(c) > 0 { 188 p.Cron = c[0] 189 p.Interval = "" 190 p.Annotations[cronAnnotation] = strings.Join(c[1:], ", ") 191 } 192 } 193 var err error 194 p.Tags, err = performReplacement(p.Tags, vars, p.Annotations[replacementAnnotation]) 195 if err != nil { 196 return nil, fmt.Errorf("%s: %w", periodic.Name, err) 197 } 198 p.Labels = performDeletion(p.Labels, p.Annotations[deletionAnnotation]) 199 p.Annotations = cleanAnnotations(fixTestgridAnnotations(p.Annotations, vars.Version, false)) 200 newPeriodics = append(newPeriodics, p) 201 } 202 return newPeriodics, nil 203 } 204 205 func cleanAnnotations(annotations map[string]string) map[string]string { 206 result := map[string]string{} 207 for k, v := range annotations { 208 if k == forkAnnotation || k == replacementAnnotation || k == deletionAnnotation { 209 continue 210 } 211 if k == periodicIntervalAnnotation && v == "" { 212 continue 213 } 214 if k == cronAnnotation && v == "" { 215 continue 216 } 217 result[k] = v 218 } 219 return result 220 } 221 222 func evaluateTemplate(s string, c interface{}) (string, error) { 223 t, err := template.New("t").Parse(s) 224 if err != nil { 225 return "", fmt.Errorf("failed to parse template %q: %w", s, err) 226 } 227 wr := bytes.Buffer{} 228 err = t.Execute(&wr, c) 229 if err != nil { 230 return "", fmt.Errorf("failed to execute template: %w", err) 231 } 232 return wr.String(), nil 233 } 234 235 func performEnvReplacement(name, value string, vars templateVars, replacements string) (string, string, error) { 236 v, err := performReplacement([]string{name + "=" + value}, vars, replacements) 237 if err != nil { 238 return "", "", err 239 } 240 if len(v) != 1 { 241 return "", "", fmt.Errorf("expected a single string result replacing env var, got %d", len(v)) 242 } 243 parts := strings.SplitN(v[0], "=", 2) 244 if len(parts) != 2 { 245 return "", "", fmt.Errorf("expected NAME=VALUE format replacing env var, got %s", v[0]) 246 } 247 return parts[0], parts[1], nil 248 } 249 250 func performReplacement(args []string, vars templateVars, replacements string) ([]string, error) { 251 if args == nil { 252 return nil, nil 253 } 254 if replacements == "" { 255 return args, nil 256 } 257 258 var rs []string 259 as := strings.Split(replacements, ", ") 260 for _, r := range as { 261 s := strings.Split(r, " -> ") 262 if len(s) != 2 { 263 return nil, fmt.Errorf("failed to parse replacement %q", r) 264 } 265 v, err := evaluateTemplate(s[1], vars) 266 if err != nil { 267 return nil, err 268 } 269 rs = append(rs, s[0], v) 270 } 271 replacer := strings.NewReplacer(rs...) 272 273 newArgs := make([]string, 0, len(args)) 274 for _, a := range args { 275 newArgs = append(newArgs, replacer.Replace(a)) 276 } 277 278 return newArgs, nil 279 } 280 281 func performDeletion(args map[string]string, deletions string) map[string]string { 282 if args == nil { 283 return nil 284 } 285 if deletions == "" { 286 return args 287 } 288 289 deletionsSet := make(map[string]bool) 290 for _, s := range strings.Split(deletions, ", ") { 291 deletionsSet[s] = true 292 } 293 294 result := map[string]string{} 295 296 for k, v := range args { 297 if !deletionsSet[k] { 298 result[k] = v 299 } 300 } 301 302 return result 303 } 304 305 const masterSuffix = "-master" 306 307 func replaceAllMaster(s, new string) string { 308 return strings.ReplaceAll(s, masterSuffix, new) 309 } 310 311 func fixImage(image, version string) string { 312 return replaceAllMaster(image, "-"+version) 313 } 314 315 func fixBootstrapArgs(args []string, version string) []string { 316 if args == nil { 317 return nil 318 } 319 replacer := strings.NewReplacer( 320 "--repo=k8s.io/kubernetes=master", "--repo=k8s.io/kubernetes=release-"+version, 321 "--repo=k8s.io/kubernetes", "--repo=k8s.io/kubernetes=release-"+version, 322 "--branch=master", "--branch=release-"+version, 323 ) 324 newArgs := make([]string, 0, len(args)) 325 for _, arg := range args { 326 newArgs = append(newArgs, replacer.Replace(arg)) 327 } 328 return newArgs 329 } 330 331 func fixExtraRefs(refs []prowapi.Refs, version string) []prowapi.Refs { 332 if refs == nil { 333 return nil 334 } 335 newRefs := make([]prowapi.Refs, 0, len(refs)) 336 for _, r := range refs { 337 if r.Org == "kubernetes" && r.Repo == "kubernetes" && r.BaseRef == "master" { 338 r.BaseRef = "release-" + version 339 } 340 if r.Org == "kubernetes" && r.Repo == "perf-tests" && r.BaseRef == "master" { 341 r.BaseRef = "release-" + version 342 } 343 newRefs = append(newRefs, r) 344 } 345 return newRefs 346 } 347 348 func fixEnvVars(vars []v1.EnvVar, version string) []v1.EnvVar { 349 if vars == nil { 350 return nil 351 } 352 newVars := make([]v1.EnvVar, 0, len(vars)) 353 for _, v := range vars { 354 if strings.Contains(strings.ToUpper(v.Name), "BRANCH") && v.Value == "master" { 355 v.Value = "release-" + version 356 } 357 newVars = append(newVars, v) 358 } 359 return newVars 360 } 361 362 func fixTestgridAnnotations(annotations map[string]string, version string, isPresubmit bool) map[string]string { 363 r := strings.NewReplacer( 364 "master-blocking", version+"-blocking", 365 "master-informing", version+"-informing", 366 ) 367 a := map[string]string{} 368 didDashboards := false 369 annotations: 370 for k, v := range annotations { 371 if isPresubmit { 372 // Forked presubmits do not get renamed, and so their annotations will be applied to master. 373 // In some cases, they will do things that are so explicitly contradictory the run will fail. 374 // Therefore, if we're forking a presubmit, just drop all testgrid config and defer to master. 375 if strings.HasPrefix(k, "testgrid-") { 376 continue 377 } 378 } 379 switch k { 380 case testgridDashboardsAnnotation: 381 fmt.Println(v) 382 v = r.Replace(v) 383 if !inOtherSigReleaseDashboard(v, version) { 384 v += ", " + "sig-release-job-config-errors" 385 } 386 didDashboards = true 387 case testgridTabNameAnnotation: 388 v = strings.ReplaceAll(v, "master", version) 389 case descriptionAnnotation: 390 continue annotations 391 } 392 a[k] = v 393 } 394 if !didDashboards && !isPresubmit { 395 a[testgridDashboardsAnnotation] = "sig-release-job-config-errors" 396 } 397 return a 398 399 } 400 401 func inOtherSigReleaseDashboard(existingDashboards, version string) bool { 402 return strings.Contains(existingDashboards, "sig-release-"+version) 403 } 404 405 func generateNameVariant(name, version string, generic bool) string { 406 suffix := "-beta" 407 if !generic { 408 suffix = "-" + strings.ReplaceAll(version, ".", "-") 409 } 410 if !strings.HasSuffix(name, masterSuffix) { 411 return name + suffix 412 } 413 return replaceAllMaster(name, suffix) 414 } 415 416 func generatePresubmitContextVariant(name, context, version string) string { 417 suffix := "-" + version 418 419 if context != "" { 420 return replaceAllMaster(context, suffix) 421 } 422 return replaceAllMaster(name, suffix) 423 } 424 425 type options struct { 426 jobConfig string 427 outputPath string 428 vars templateVars 429 } 430 431 type templateVars struct { 432 Version string 433 GoVersion string 434 } 435 436 func parseFlags() options { 437 o := options{} 438 flag.StringVar(&o.jobConfig, "job-config", "", "Path to the job config") 439 flag.StringVar(&o.outputPath, "output", "", "Path to the output yaml. if not specified, just validate.") 440 flag.StringVar(&o.vars.Version, "version", "", "Version number to generate jobs for") 441 flag.StringVar(&o.vars.GoVersion, "go-version", "", "Current go version in use; see http://git.k8s.io/kubernetes/.go-version") 442 flag.Parse() 443 return o 444 } 445 446 func validateOptions(o options) error { 447 if o.jobConfig == "" { 448 return errors.New("--job-config must be specified") 449 } 450 if o.vars.Version == "" { 451 return errors.New("--version must be specified") 452 } 453 if match, err := regexp.MatchString(`^\d+\.\d+$`, o.vars.Version); err != nil || !match { 454 return fmt.Errorf("%q doesn't look like a valid version number", o.vars.Version) 455 } 456 if o.vars.GoVersion == "" { 457 return errors.New("--go-version must be specified; http://git.k8s.io/kubernetes/.go-version contains the recommended value") 458 } 459 if match, err := regexp.MatchString(`^\d+\.\d+(\.\d+)?(rc\d)?$`, o.vars.GoVersion); err != nil || !match { 460 return fmt.Errorf("%q doesn't look like a valid go version; should match the format 1.20rc1, 1.20, or 1.20.2", o.vars.GoVersion) 461 } 462 return nil 463 } 464 465 func main() { 466 o := parseFlags() 467 if err := validateOptions(o); err != nil { 468 log.Fatalln(err) 469 } 470 c, err := config.ReadJobConfig(o.jobConfig) 471 if err != nil { 472 log.Fatalf("Failed to load job config: %v\n", err) 473 } 474 475 newPresubmits, err := generatePresubmits(c, o.vars) 476 if err != nil { 477 log.Fatalf("Failed to generate presubmits: %v.\n", err) 478 } 479 newPeriodics, err := generatePeriodics(c, o.vars) 480 if err != nil { 481 log.Fatalf("Failed to generate periodics: %v.\n", err) 482 } 483 newPostsubmits, err := generatePostsubmits(c, o.vars) 484 if err != nil { 485 log.Fatalf("Failed to generate postsubmits: %v.\n", err) 486 } 487 488 // We need to use FutureLineWrap because "fork-per-release-cron" is too long 489 // causing the annotation value to be split into two lines. 490 // We use gopkg.in/yaml here because sigs.k8s.io/yaml doesn't export this 491 // function. sigs.k8s.io/yaml uses gopkg.in/yaml under the hood. 492 gyaml.FutureLineWrap() 493 494 output, err := yaml.Marshal(map[string]interface{}{ 495 "periodics": newPeriodics, 496 "presubmits": newPresubmits, 497 "postsubmits": newPostsubmits, 498 }) 499 if err != nil { 500 log.Fatalf("Failed to marshal new presubmits: %v\n", err) 501 } 502 503 if o.outputPath != "" { 504 if err := os.WriteFile(o.outputPath, output, 0666); err != nil { 505 log.Fatalf("Failed to write new presubmits: %v.\n", err) 506 } 507 } else { 508 log.Println("No output file specified, so not writing anything.") 509 } 510 }