diff --git a/.github/ISSUE_TEMPLATE/kubernetes_bump.md b/.github/ISSUE_TEMPLATE/kubernetes_bump.md index 9b65aec8d365..9834458bef4c 100644 --- a/.github/ISSUE_TEMPLATE/kubernetes_bump.md +++ b/.github/ISSUE_TEMPLATE/kubernetes_bump.md @@ -34,21 +34,25 @@ changes should be cherry-picked to all release series that will support the new * Note: Only bump for Cluster API versions that will support the new Kubernetes release. * Prior art: #9160 * [ ] Ensure the jobs are adjusted to provide test coverage according to our [support policy](https://cluster-api.sigs.k8s.io/reference/versions.html#supported-kubernetes-versions): - * For the main branch: - * periodics: - * Drop the oldest upgrade job as the oldest Kubernetes minor version is now out of support. - * Add new upgrade job which upgrades from the previous to the new Kubernetes version. - * periodics & presubmits: - * Bump `KUBERNETES_VERSION_MANAGEMENT` of the `e2e-mink8s` job to the new minimum supported management cluster version. - * Bump `KUBEBUILDER_ENVTEST_KUBERNETES_VERSION` of the `test-mink8s` jobs to the new minimum supported management cluster version. - * Adjust the `-latest` upgrade job to upgrade from the new Kubernetes to the next Kubernetes version. - * For the release branch of the latest supported Cluster API minor release: - * periodics & presubmits: - * Adust the `-latest` upgrade jobs to upgrade to the new Kubernetes version instead of latest. - * Note: Also check if `ETCD_VERSION_UPGRADE_TO` or `COREDNS_VERSION_UPGRADE_TO` needs to change for the upgrades jobs to the new or next Kubernetes version. - * For etcd, see the `DefaultEtcdVersion` kubeadm constant: [e.g. for v1.28.0](https://github.com/kubernetes/kubernetes/blob/v1.28.0/cmd/kubeadm/app/constants/constants.go#L308) - * For coredns, see the `CoreDNSVersion` kubeadm constant:[e.g. for v1.28.0](https://github.com/kubernetes/kubernetes/blob/v1.28.0/cmd/kubeadm/app/constants/constants.go#L344) - * Prior art: https://github.com/kubernetes/test-infra/pull/30347 https://github.com/kubernetes/test-infra/pull/30406 https://github.com/kubernetes/test-infra/pull/30407 + + * At the `.versions` section in the `cluster-api-prowjob-gen.yaml` file in [test-infra](https://github.com/kubernetes/test-infra/blob/master/config/jobs/kubernetes-sigs/cluster-api/): + * Add a new entry for the new Kubernetes version + * Adjust the released Kubernetes's version entry to refer `stable-1.` instead of `ci/latest-1.` + * Check and update the versions for the keys `etcd` and `coreDNS` if necessary: + * For etcd, see the `DefaultEtcdVersion` kubeadm constant: [e.g. for v1.28.0](https://github.com/kubernetes/kubernetes/blob/v1.28.0/cmd/kubeadm/app/constants/constants.go#L308) + * For coredns, see the `CoreDNSVersion` kubeadm constant:[e.g. for v1.28.0](https://github.com/kubernetes/kubernetes/blob/v1.28.0/cmd/kubeadm/app/constants/constants.go#L344) + * For the `.branches.main` section in the `cluster-api-prowjob-gen.yaml` file in [test-infra](https://github.com/kubernetes/test-infra/blob/master/config/jobs/kubernetes-sigs/cluster-api/): + * For the `.upgrades` section: + * Drop the oldest upgrade + * Add a new upgrade entry from the previous to the new Kubernetes version + * Bump the version set at `.kubernetesVersionManagement` to the new minimum supported management cluster version (This is the image version available as kind image). + * Bump the version set at `.kubebuilderEnvtestKubernetesVersion` to the new minimum supported management cluster version. + * Run `make generate-test-infra-prowjobs` to generate the resulting prowjob configuration: + + ```sh + TEST_INFRA_DIR=../../k8s.io/test-infra make generate-test-infra-prowjobs + ``` + * [ ] Update book: * Update supported versions in `versions.md` * Update job documentation in `jobs.md` @@ -65,7 +69,7 @@ need them in older releases as they are not necessary to manage workload cluster run the Cluster API controllers on the new Kubernetes version. * [ ] Ensure there is a new controller-runtime minor release which uses the new Kubernetes Go dependencies. -* [ ] Update our Prow jobs for the `main` branch to use the correct `kubekins-e2e` image +* [ ] Update our Prow jobs for the `main` branch to use the correct `kubekins-e2e` image via the configuration file and by running `make generate-test-infra-prowjobs`. * It is recommended to have one PR for presubmit and one for periodic jobs to reduce the risk of breaking the periodic jobs. * Prior art: presubmit jobs: https://github.com/kubernetes/test-infra/pull/27311 * Prior art: periodic jobs: https://github.com/kubernetes/test-infra/pull/27326 diff --git a/Makefile b/Makefile index e4172d16c715..c18e2abcae3d 100644 --- a/Makefile +++ b/Makefile @@ -190,6 +190,9 @@ OPENAPI_GEN_BIN := openapi-gen OPENAPI_GEN := $(abspath $(TOOLS_BIN_DIR)/$(OPENAPI_GEN_BIN)) OPENAPI_GEN_PKG := k8s.io/kube-openapi/cmd/openapi-gen +PROWJOB_GEN_BIN := prowjob-gen +PROWJOB_GEN := $(abspath $(TOOLS_BIN_DIR)/$(PROWJOB_GEN_BIN)) + RUNTIME_OPENAPI_GEN_BIN := runtime-openapi-gen RUNTIME_OPENAPI_GEN := $(abspath $(TOOLS_BIN_DIR)/$(RUNTIME_OPENAPI_GEN_BIN)) @@ -600,6 +603,13 @@ generate-diagrams-book: ## Generate diagrams for *.plantuml files in book generate-diagrams-proposals: ## Generate diagrams for *.plantuml files in proposals docker run -v $(ROOT_DIR)/$(DOCS_DIR):/$(DOCS_DIR)$(DOCKER_VOL_OPTS) plantuml/plantuml:$(PLANTUML_VER) /$(DOCS_DIR)/proposals/**/*.plantuml +.PHONY: generate-test-infra-prowjobs +generate-test-infra-prowjobs: $(PROWJOB_GEN) ## Generates the prowjob configurations in test-infra + @if [ -z "${TEST_INFRA_DIR}" ]; then echo "TEST_INFRA_DIR is not set"; exit 1; fi + $(PROWJOB_GEN) \ + -config "$(TEST_INFRA_DIR)/config/jobs/kubernetes-sigs/cluster-api/cluster-api-prowjob-gen.yaml" \ + -templates-dir "$(TEST_INFRA_DIR)/config/jobs/kubernetes-sigs/cluster-api/templates" \ + -output-dir "$(TEST_INFRA_DIR)/config/jobs/kubernetes-sigs/cluster-api" ## -------------------------------------- ## Lint / Verify @@ -1307,6 +1317,9 @@ $(OPENAPI_GEN_BIN): $(OPENAPI_GEN) ## Build a local copy of openapi-gen. .PHONY: $(RUNTIME_OPENAPI_GEN_BIN) $(RUNTIME_OPENAPI_GEN_BIN): $(RUNTIME_OPENAPI_GEN) ## Build a local copy of runtime-openapi-gen. +.PHONY: $(PROWJOB_GEN_BIN) +$(PROWJOB_GEN_BIN): $(PROWJOB_GEN) ## Build a local copy of prowjob-gen. + .PHONY: $(CONVERSION_VERIFIER_BIN) $(CONVERSION_VERIFIER_BIN): $(CONVERSION_VERIFIER) ## Build a local copy of conversion-verifier. @@ -1367,6 +1380,10 @@ $(OPENAPI_GEN): # Build openapi-gen from tools folder. $(RUNTIME_OPENAPI_GEN): $(TOOLS_DIR)/go.mod # Build openapi-gen from tools folder. cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/$(RUNTIME_OPENAPI_GEN_BIN) sigs.k8s.io/cluster-api/hack/tools/runtime-openapi-gen +.PHONY: $(PROWJOB_GEN) +$(PROWJOB_GEN): $(TOOLS_DIR)/go.mod # Build prowjob-gen from tools folder. + cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/$(PROWJOB_GEN_BIN) sigs.k8s.io/cluster-api/hack/tools/prowjob-gen + $(GOTESTSUM): # Build gotestsum from tools folder. GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GOTESTSUM_PKG) $(GOTESTSUM_BIN) $(GOTESTSUM_VER) diff --git a/docs/release/release-tasks.md b/docs/release/release-tasks.md index 23436b13c96e..00f142b6c2a4 100644 --- a/docs/release/release-tasks.md +++ b/docs/release/release-tasks.md @@ -477,22 +477,15 @@ The goal of this task is to have test coverage for the new release branch and re While we add test coverage for the new release branch we will also drop the tests for old release branches if necessary. 1. Create new jobs based on the jobs running against our `main` branch: - 1. Copy `test-infra/config/jobs/kubernetes-sigs/cluster-api/cluster-api-periodics-main.yaml` to `config/jobs/kubernetes-sigs/cluster-api/cluster-api-periodics-release-1-6.yaml`. - 2. Copy `test-infra/config/jobs/kubernetes-sigs/cluster-api/cluster-api-periodics-main-upgrades.yaml` to `test-infra/config/jobs/kubernetes-sigs/cluster-api/cluster-api-periodics-release-1-6-upgrades.yaml`. - 3. Copy `test-infra/config/jobs/kubernetes-sigs/cluster-api/cluster-api-presubmits-main.yaml` to `test-infra/config/jobs/kubernetes-sigs/cluster-api/cluster-api-presubmits-release-1-6.yaml`. - 4. Modify the following: - 1. Rename the jobs, e.g.: `periodic-cluster-api-test-main` => `periodic-cluster-api-test-release-1-6`. - 2. Change `annotations.testgrid-dashboards` to `sig-cluster-lifecycle-cluster-api-1.6`. - 3. Change `annotations.testgrid-tab-name`, e.g. `capi-test-main` => `capi-test-release-1-6`. - 4. For periodics additionally: - * Change `extra_refs[].base_ref` to `release-1.6` (for repo: `cluster-api`). - * Change interval (let's use the same as for `1.5`). - 5. For presubmits additionally: Adjust branches: `^main$` => `^release-1.6$`. + 1. Copy the `main` branch entry as `release-1.6` in the `cluster-api-prowjob-gen.yaml` file in [test-infra](https://github.com/kubernetes/test-infra/blob/master/config/jobs/kubernetes-sigs/cluster-api/). + 2. Modify the following at the `release-1.6` branch entry: + * Change intervals (let's use the same as for `release-1.5`). 2. Create a new dashboard for the new branch in: `test-infra/config/testgrids/kubernetes/sig-cluster-lifecycle/config.yaml` (`dashboard_groups` and `dashboards`). -3. Remove tests from the [test-infra](https://github.com/kubernetes/test-infra) repository for old release branches according to our policy documented in [Support and guarantees](../../CONTRIBUTING.md#support-and-guarantees). For example, let's assume we just created tests for v1.6, then we can now drop test coverage for the release-1.3 branch. -4. Verify the jobs and dashboards a day later by taking a look at: `https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api-1.6` -5. Update `.github/workflows/weekly-security-scan.yaml` - to setup Trivy and govulncheck scanning - `.github/workflows/weekly-md-link-check.yaml` - to setup link checking in the CAPI book - and `.github/workflows/weekly-test-release.yaml` - to verify the release target is working - for the currently supported branches. -6. Update the [PR markdown link checker](https://github.com/kubernetes-sigs/cluster-api/blob/main/.github/workflows/pr-md-link-check.yaml) accordingly (e.g. `main` -> `release-1.6`). +3. Remove old release branches and unused versions from the `cluster-api-prowjob-gen.yaml` file in [test-infra](https://github.com/kubernetes/test-infra/blob/master/config/jobs/kubernetes-sigs/cluster-api/) according to our policy documented in [Support and guarantees](../../CONTRIBUTING.md#support-and-guarantees). For example, let's assume we just added `release-1.6`, then we can now drop test coverage for the `release-1.3` branch. +4. Regenerate the probjob configuration using `make generate-test-infra-prowjobs`. +5. Verify the jobs and dashboards a day later by taking a look at: `https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api-1.6` +6. Update `.github/workflows/weekly-security-scan.yaml` - to setup Trivy and govulncheck scanning - `.github/workflows/weekly-md-link-check.yaml` - to setup link checking in the CAPI book - and `.github/workflows/weekly-test-release.yaml` - to verify the release target is working - for the currently supported branches. +7. Update the [PR markdown link checker](https://github.com/kubernetes-sigs/cluster-api/blob/main/.github/workflows/pr-md-link-check.yaml) accordingly (e.g. `main` -> `release-1.6`).
Prior art: [Update branch for link checker](https://github.com/kubernetes-sigs/cluster-api/pull/9206) diff --git a/hack/boilerplate/boilerplate.py b/hack/boilerplate/boilerplate.py index 830fcdd3e842..8c62bd4895cd 100755 --- a/hack/boilerplate/boilerplate.py +++ b/hack/boilerplate/boilerplate.py @@ -143,6 +143,7 @@ def file_passes(filename, refs, regexs): for line in difflib.unified_diff(ref, data, 'reference', filename, lineterm=''): print(line, file=verbose_out) print(file=verbose_out) + return False return True @@ -154,7 +155,12 @@ def file_extension(filename): # list all the files contain 'DO NOT EDIT', but are not generated skipped_ungenerated_files = [ - 'hack/lib/swagger.sh', 'hack/boilerplate/boilerplate.py'] + 'hack/lib/swagger.sh', + 'hack/boilerplate/boilerplate.py', + # The generator injects `DO NOT EDIT` and thus needs to get excluded to not + # get detected as false positive. + 'hack/tools/prowjob-gen/generator.go', + ] def normalize_files(files): newfiles = [] diff --git a/hack/tools/go.mod b/hack/tools/go.mod index f3bfd7734127..ec632a37ce1c 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -9,6 +9,7 @@ replace sigs.k8s.io/cluster-api/test => ../../test require ( cloud.google.com/go/storage v1.36.0 github.com/blang/semver/v4 v4.0.0 + github.com/google/go-cmp v0.6.0 github.com/onsi/gomega v1.30.0 github.com/pkg/errors v0.9.1 github.com/spf13/pflag v1.0.5 @@ -71,7 +72,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/cel-go v0.17.7 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect diff --git a/hack/tools/prowjob-gen/README.md b/hack/tools/prowjob-gen/README.md new file mode 100644 index 000000000000..f446c5e422f2 --- /dev/null +++ b/hack/tools/prowjob-gen/README.md @@ -0,0 +1,71 @@ +# prowjob-gen + +Prowjob-gen is a tool which helps generating prowjob configuration. + +## Usage + +Flags: + +```txt + -config string + Path to the config file + -output-dir string + Path to the directory to create the files in + -templates-dir string + Path to the directory containing the template files referenced inside the config file +``` + +When running prowjob-gen, all flags need to be provided. +The tool then will iterate over all templates defined in the config file and execute them per configured branch. + +The configuration file is supposed to be in yaml format and to be stored inside the [test-infra](https://github.com/kubernetes/test-infra) +repository, we have to make sure it is not getting parsed as configuration for prow. +Because of that the top-level key for the configuration file is `prow-ignored:`. + +A sample configuration looks as follows: + + +```yaml +prow_ignored: + branches: + main: # values below the branch here are available in the template + testImage: "gcr.io/k8s-staging-test-infra/kubekins-e2e:v20231208-8b9fd88e88-1.29" + interval: "2h" + upgradesInterval: "2h" + kubernetesVersionManagement: "v1.26.6@sha256:6e2d8b28a5b601defe327b98bd1c2d1930b49e5d8c512e1895099e4504007adb" + kubebuilderEnvtestKubernetesVersion: "1.26.1" + upgrades: + - from: "1.29" + to: "1.30" + + templates: + - name: "test.yaml.tpl" + template: "test-{{ .branch }}.yaml.tmp" + + versions: + "1.29": + etcd: "3.5.10-0" + coreDNS: "v1.11.1" + k8sRelease: "stable-1.29" + "1.30": + etcd: "3.5.10-0" + coreDNS: "v1.11.1" + k8sRelease: "ci/latest-1.30" +``` + + +With this configuration, the template `cluster-api-periodics.yaml.tpl` would get executed for each branch. +In this example we only configure the `main` branch which results in the output file `cluster-api-periodics-main.yaml`. + +When executing a template, the following functions are available as addition to the standard functions in go templates: + +- `TrimPrefix`: [strings.TrimPrefix](https://pkg.go.dev/strings#TrimPrefix) +- `TrimSuffix`: [strings.TrimSuffix](https://pkg.go.dev/strings#TrimSuffix) +- `ReplaceAll`: [strings.ReplaceAll](https://pkg.go.dev/strings#ReplaceAll) +- `last`: `func(any) any`: returns the last element of an array or slice. + +When executing a template, the following variables are available: + +- `branch`: The branch name the file gets templated for (The key in `.prow_ignored.branches`). +- `config`: The branch's configuration from `.prow_ignored.branches.`. +- `versions`: The versions mapper from `.prow_ignored.versions`. diff --git a/hack/tools/prowjob-gen/config.go b/hack/tools/prowjob-gen/config.go new file mode 100644 index 000000000000..5960fe93fd16 --- /dev/null +++ b/hack/tools/prowjob-gen/config.go @@ -0,0 +1,57 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +// ProwIgnoredConfig is the top-level configuration struct. Because we want to +// store the configuration in test-infra as yaml file, we have to prevent prow +// from trying to parse our configuration as prow configuration. Prow provides +// the well-known `prow_ignored` key which is not parsed further by Prow. +type ProwIgnoredConfig struct { + ProwIgnored Config `json:"prow_ignored"` +} + +// Config is the configuration file struct. +type Config struct { + Branches map[string]BranchConfig `json:"branches"` + Templates []Template `json:"templates"` + Versions VersionsMapper `json:"versions"` +} + +// BranchConfig is the branch-based configuration struct. +type BranchConfig struct { + Interval string `json:"interval"` + UpgradesInterval string `json:"upgradesInterval"` + TestImage string `json:"testImage"` + KubernetesVersionManagement string `json:"kubernetesVersionManagement"` + KubebuilderEnvtestKubernetesVersion string `json:"kubebuilderEnvtestKubernetesVersion"` + Upgrades []*Upgrade `json:"upgrades"` +} + +// Template refers a template file and defines the target file name template. +type Template struct { + Template string `json:"template"` + Name string `json:"name"` +} + +// Upgrade describes a kubernetes upgrade. +type Upgrade struct { + From string `json:"from"` + To string `json:"to"` +} + +// VersionsMapper provides key value pairs for a parent key. +type VersionsMapper map[string]map[string]string diff --git a/hack/tools/prowjob-gen/generator.go b/hack/tools/prowjob-gen/generator.go new file mode 100644 index 000000000000..420f0a542c72 --- /dev/null +++ b/hack/tools/prowjob-gen/generator.go @@ -0,0 +1,192 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "fmt" + "os" + "path" + "path/filepath" + "reflect" + "strings" + "text/template" + + "github.com/pkg/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/yaml" +) + +const generatedFileHeader = "# Code generated by cluster-api's prowjob-gen. DO NOT EDIT.\n" + +// newGenerator initializes a generator which includes parsing the configured templates. +func newGenerator(configFile string, templatesDir, outputDir string) (*generator, error) { + // Read and Unmarshal the configuration file. + rawConfig, err := os.ReadFile(filepath.Clean(configFile)) + if err != nil { + return nil, errors.Wrapf(err, "reading config file %s", configFile) + } + prowIgnoredConfig := ProwIgnoredConfig{} + if err := yaml.UnmarshalStrict(rawConfig, &prowIgnoredConfig); err != nil { + return nil, errors.Wrapf(err, "parsing config file %s", configFile) + } + + g := &generator{ + config: prowIgnoredConfig.ProwIgnored, + outputDir: outputDir, + nameTemplates: map[string]*template.Template{}, + } + + g.templates, err = template.New(""). + Funcs(g.templateFunctions()). + ParseGlob(templatesDir + "/*.yaml.tpl") + if err != nil { + return nil, err + } + + for _, tpl := range g.config.Templates { + nameTemplate, err := template.New("").Funcs(g.templateFunctions()).Parse(tpl.Template) + if err != nil { + return nil, errors.Wrapf(err, "parsing name template %q", tpl.Template) + } + g.nameTemplates[tpl.Name] = nameTemplate + } + + return g, nil +} + +type generator struct { + templates *template.Template + nameTemplates map[string]*template.Template + config Config + outputDir string +} + +// generate executes every template for every branch and writes the result to a +// file in outputDir. +func (g *generator) generate() error { + for _, tpl := range g.config.Templates { + for branch := range g.config.Branches { + klog.Infof("Executing and writing template %q for branch %q", tpl.Name, branch) + out, err := g.executeTemplate(branch, tpl.Name) + if err != nil { + return errors.Wrapf(err, "Generating prowjobs for template %s", tpl.Name) + } + + fileName, err := g.executeNameTemplate(branch, tpl.Name) + if err != nil { + return errors.Wrapf(err, "Generating name for template %s and branch %s", tpl.Name, branch) + } + filePath := filepath.Clean(path.Join(g.outputDir, fileName)) + if err := os.WriteFile(filePath, out.Bytes(), 0644); err != nil { //nolint:gosec + return errors.Wrapf(err, "Writing prowjob to %q", filePath) + } + } + } + return nil +} + +// cleanup deletes files which have the generatedFileHeader. +func (g *generator) cleanup() error { + entries, err := os.ReadDir(g.outputDir) + if err != nil { + return err + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + path := filepath.Clean(path.Join(g.outputDir, entry.Name())) + data, err := os.ReadFile(path) + if err != nil { + return err + } + + if strings.HasPrefix(string(data), generatedFileHeader) { + klog.Infof("Deleting file %s", entry.Name()) + if err := os.Remove(path); err != nil { + return err + } + } + } + + return nil +} + +// executeTemplate executes a previously parsed template with the data for a specific branch. +func (g *generator) executeTemplate(branch, templateName string) (*bytes.Buffer, error) { + data := map[string]interface{}{ + "branch": branch, + "config": g.config.Branches[branch], + "versions": g.config.Versions, + } + + var out bytes.Buffer + + // Write yaml comment as header to indicate this file got generated. + out.WriteString(generatedFileHeader) + + if err := g.templates.ExecuteTemplate(&out, templateName, data); err != nil { + return nil, errors.Wrapf(err, "executing template %q for branch %q", templateName, branch) + } + + return &out, nil +} + +// executeNameTemplate executes a previously parsed name template with the branch as data. +func (g *generator) executeNameTemplate(branch, templateName string) (string, error) { + data := map[string]interface{}{ + "branch": branch, + } + + var out bytes.Buffer + + if err := g.nameTemplates[templateName].Execute(&out, data); err != nil { + return "", errors.Wrapf(err, "executing name template %q for branch %q", templateName, branch) + } + + return out.String(), nil +} + +// templateFunctions returns the functions available inside of templates. +func (g *generator) templateFunctions() template.FuncMap { + funcs := template.FuncMap{} + funcs["TrimPrefix"] = strings.TrimPrefix + funcs["TrimSuffix"] = strings.TrimSuffix + funcs["ReplaceAll"] = strings.ReplaceAll + funcs["last"] = last + return funcs +} + +func last(list any) any { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + + l := l2.Len() + if l == 0 { + return nil + } + + return l2.Index(l - 1).Interface() + default: + panic(fmt.Sprintf("Cannot find last on type %s", tp)) + } +} diff --git a/hack/tools/prowjob-gen/main.go b/hack/tools/prowjob-gen/main.go new file mode 100644 index 000000000000..9611f5c65e74 --- /dev/null +++ b/hack/tools/prowjob-gen/main.go @@ -0,0 +1,60 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// main is the main package for prowjob-gen. +package main + +import ( + "flag" + + "k8s.io/klog/v2" +) + +var ( + configFile = flag.String("config", "", "Path to the config file") + outputDir = flag.String("output-dir", "", "Path to the directory to create the files in") + templatesDir = flag.String("templates-dir", "", "Path to the directory containing the template files referenced inside the config file") +) + +func main() { + // Parse flags and validate input. + flag.Parse() + if *configFile == "" { + klog.Fatal("Expected flag \"config\" to be set") + } + if *outputDir == "" { + klog.Fatal("Expected flag \"output-dir\" to be set") + } + if *templatesDir == "" { + klog.Fatal("Expected flag \"templates-dir\" to be set") + } + + // Initialize a generator using the config data. + g, err := newGenerator(*configFile, *templatesDir, *outputDir) + if err != nil { + klog.Fatalf("Failed to initialize generator: %v", err) + } + + // Cleanup old files. + if err := g.cleanup(); err != nil { + klog.Fatalf("Failed to cleanup old generated files: %v", err) + } + + // Generate new files. + if err := g.generate(); err != nil { + klog.Fatalf("Failed to generate prowjobs: %v", err) + } +} diff --git a/hack/tools/prowjob-gen/main_test.go b/hack/tools/prowjob-gen/main_test.go new file mode 100644 index 000000000000..ed5246d8ea14 --- /dev/null +++ b/hack/tools/prowjob-gen/main_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// main is the main package for prowjob-gen. +package main + +import ( + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Generator(t *testing.T) { + g, err := newGenerator("test/test-configuration.yaml", "test", "test") + if err != nil { + t.Errorf("newGenerator() error = %v", err) + return + } + + if err := g.generate(); err != nil { + t.Errorf("g.generate() error = %v", err) + return + } + + goldenData, err := os.ReadFile("test/test-main.yaml.golden") + if err != nil { + t.Errorf("reading golden file: %v", err) + return + } + + testData, err := os.ReadFile("test/test-main.yaml.tmp") + if err != nil { + t.Errorf("reading file generated from test: %v", err) + return + } + + if diff := cmp.Diff(string(goldenData), string(testData)); diff != "" { + t.Errorf("generated and golden test file differ:\n%s", diff) + } +} + +const configFileDelimiter = "" + +func Test_testConfiguration(t *testing.T) { + rawReadme, err := os.ReadFile("README.md") + if err != nil { + t.Errorf("reading file README.md: %v", err) + return + } + + splitted := strings.Split(string(rawReadme), configFileDelimiter) + if len(splitted) != 3 { + t.Errorf("expected README.md to contain %q twice", configFileDelimiter) + } + + rawConfiguration, err := os.ReadFile("test/test-configuration.yaml") + if err != nil { + t.Errorf("reading file test/test-configuration.yaml: %v", err) + return + } + + readmeConfiguration := strings.TrimPrefix(string(rawConfiguration), "\n``yaml\n") + readmeConfiguration = strings.TrimSuffix(readmeConfiguration, "```\n") + + if diff := cmp.Diff(readmeConfiguration, string(rawConfiguration)); diff != "" { + t.Errorf("Configuration in README.md and test/test-configuration.yaml differ:\n%s", diff) + } +} diff --git a/hack/tools/prowjob-gen/test/test-configuration.yaml b/hack/tools/prowjob-gen/test/test-configuration.yaml new file mode 100644 index 000000000000..fefb2b07be61 --- /dev/null +++ b/hack/tools/prowjob-gen/test/test-configuration.yaml @@ -0,0 +1,25 @@ +prow_ignored: + branches: + main: # values below the branch here are available in the template + testImage: "gcr.io/k8s-staging-test-infra/kubekins-e2e:v20231208-8b9fd88e88-1.29" + interval: "2h" + upgradesInterval: "2h" + kubernetesVersionManagement: "v1.26.6@sha256:6e2d8b28a5b601defe327b98bd1c2d1930b49e5d8c512e1895099e4504007adb" + kubebuilderEnvtestKubernetesVersion: "1.26.1" + upgrades: + - from: "1.29" + to: "1.30" + + templates: + - name: "test.yaml.tpl" + template: "test-{{ .branch }}.yaml.tmp" + + versions: + "1.29": + etcd: "3.5.10-0" + coreDNS: "v1.11.1" + k8sRelease: "stable-1.29" + "1.30": + etcd: "3.5.10-0" + coreDNS: "v1.11.1" + k8sRelease: "ci/latest-1.30" diff --git a/hack/tools/prowjob-gen/test/test-main.yaml.golden b/hack/tools/prowjob-gen/test/test-main.yaml.golden new file mode 100644 index 000000000000..39baab837639 --- /dev/null +++ b/hack/tools/prowjob-gen/test/test-main.yaml.golden @@ -0,0 +1,6 @@ +# Code generated by cluster-api's prowjob-gen. DO NOT EDIT. +main +main +bar +foobar +1.29 \ No newline at end of file diff --git a/hack/tools/prowjob-gen/test/test.yaml.tpl b/hack/tools/prowjob-gen/test/test.yaml.tpl new file mode 100644 index 000000000000..451d98754b0e --- /dev/null +++ b/hack/tools/prowjob-gen/test/test.yaml.tpl @@ -0,0 +1,5 @@ +{{ .branch }} +{{ ReplaceAll .branch "." "-" }} +{{ TrimPrefix "foobar" "foo" }} +{{ TrimPrefix "foobar" "bar" }} +{{ (last $.config.Upgrades).From }} \ No newline at end of file