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  }