Skip to content

Commit

Permalink
hack: implement prowjob-generator
Browse files Browse the repository at this point in the history
prowjob-generator allows to generate the periodic and presubmit configuration files in test-infra
from a configuration file which simplifies and automates introducing and chaning tests for branches.
  • Loading branch information
chrischdi committed Jan 11, 2024
1 parent 9f124ad commit 819c031
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 17 deletions.
36 changes: 20 additions & 16 deletions .github/ISSUE_TEMPLATE/kubernetes_bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 kKubernetes's version entry to refer `stable-1.<minor>` instead of `ci/latest-1.<minor>`
* 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`
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)

Expand Down
6 changes: 5 additions & 1 deletion hack/boilerplate/boilerplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -154,7 +155,10 @@ 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',
'/hack/tools/prowjob-gen/generator.go',
]

def normalize_files(files):
newfiles = []
Expand Down
2 changes: 2 additions & 0 deletions hack/tools/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ replace sigs.k8s.io/cluster-api/test => ../../test

require (
cloud.google.com/go/storage v1.36.0
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/blang/semver/v4 v4.0.0
github.com/onsi/gomega v1.30.0
github.com/pkg/errors v0.9.1
Expand Down Expand Up @@ -37,6 +38,7 @@ require (
cloud.google.com/go/iam v1.1.5 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions hack/tools/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
Expand Down
56 changes: 56 additions & 0 deletions hack/tools/prowjob-gen/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
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"`
VersionsMapper VersionsMapper `json:"versions"`
}

// BranchConfig is the branch-based configuration struct.
type BranchConfig struct {
Interval string `json:"interval"`
KubekinsImage string `json:"kubekinsImage"`
KubernetesVersionManagement string `json:"kubernetesVersionManagement"`
KubebuilderEnvtestKubernetesVersion string `json:"kubebuilderEnvtestKubernetesVersion"`
Upgrades []Upgrade `json:"upgrades"`
}

// Template refers a template file and defines the target file name format.
type Template struct {
Format string `json:"format"`
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
163 changes: 163 additions & 0 deletions hack/tools/prowjob-gen/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
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"
"strings"
"text/template"

"github.com/Masterminds/sprig"
"github.com/pkg/errors"
"k8s.io/klog/v2"
)

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(config Config, templatesDir, outputDir string) (*generator, error) {
g := &generator{
config: config,
outputDir: outputDir,
createdFiles: map[string]bool{},
}

var err error
g.templates, err = template.New("").
Funcs(g.templateFunctions()).
ParseGlob(templatesDir + "/*.yaml.tpl")
if err != nil {
return nil, err
}

return g, err
}

type generator struct {
templates *template.Template
config Config
outputDir string
createdFiles map[string]bool
}

// 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 {
out, err := g.executeTemplate(branch, tpl.Name)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrapf(err, "Generating prowjobs for template %s", tpl.Name)
}

fileName := fmt.Sprintf(tpl.Format, strings.ReplaceAll(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)
}

g.createdFiles[fileName] = true
}
}
return nil
}

// cleanup deletes files which have the generatedFileHeader and had not been updated
// during generate.
func (g *generator) cleanup() error {
entries, err := os.ReadDir(g.outputDir)
if err != nil {
return err
}

for _, entry := range entries {
if _, ok := g.createdFiles[entry.Name()]; ok {
continue
}

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) {
klog.Infof("executing template %q for branch %q", templateName, branch)

data := map[string]interface{}{
"branch": branch,
"config": g.config.Branches[branch],
}

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
}

// templateFunctions returns the functions available inside of templates.
func (g *generator) templateFunctions() template.FuncMap {
funcs := sprig.HermeticTxtFuncMap()
funcs["versionMapperLookup"] = g.versionMapperLookup
funcs["lastUpgradeVersion"] = g.lastUpgradeVersion
return funcs
}

// versionMapperLookup returns a value from the versions mapper for a given version and key.
func (g *generator) versionMapperLookup(version, key string) string {
v, ok := g.config.VersionsMapper[version]
if !ok {
klog.Fatalf("Failed to lookup version (%q) in config", version)
}
c, ok := v[key]
if !ok {
klog.Fatalf("Failed to lookup component version (%q) for version %q in config", key, version)
}
return c
}

// lastUpgradeVersion returns the last Upgrade entry in the Upgrades slice for a given branch.
func (g *generator) lastUpgradeVersion(branch string) Upgrade {
upgrades := g.config.Branches[branch].Upgrades
return upgrades[len(upgrades)-1]
}
Loading

0 comments on commit 819c031

Please sign in to comment.