Skip to content

Commit d24da35

Browse files
committed
[chore] add logic to update our tests k8s versions
Signed-off-by: Dani Louca <[email protected]>
1 parent fd08bd1 commit d24da35

File tree

6 files changed

+470
-0
lines changed

6 files changed

+470
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Check for new matrix versions and update tests if needed
2+
3+
on:
4+
schedule:
5+
# Run every Monday at noon.
6+
- cron: "0 12 * * 1"
7+
workflow_dispatch:
8+
inputs:
9+
DEBUG_ARGUMENT:
10+
description: 'Enable debug by setting -debug to true'
11+
required: false
12+
default: '-debug=false'
13+
14+
jobs:
15+
check_and_update:
16+
runs-on: ubuntu-latest
17+
env:
18+
DEBUG: ${{ github.event.inputs.DEBUG_ARGUMENT }}
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v4
22+
23+
- name: Check for new matrix versions
24+
id: check_for_update
25+
run: |
26+
echo "Checking for new matrix versions"
27+
make update-matrix-versions DEBUG=$DEBUG
28+
29+
- name: check for changes
30+
id: git-check
31+
run: |
32+
if git diff --quiet; then
33+
echo "No changes detected, exiting workflow successfully"
34+
exit 0
35+
fi
36+
echo "changes=true" >> $GITHUB_OUTPUT
37+
38+
- name: Open PR for matrix version update
39+
if: steps.git-check.outputs.changes == 'true'
40+
uses: peter-evans/create-pull-request@v7
41+
with:
42+
commit-message: Update matrix test versions
43+
title: Update matrix versions used for testing
44+
body: Use latest supported matrix versions
45+
branch: update-matrix-test-versions
46+
base: main
47+
delete-branch: true

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,7 @@ update-operator-crds: ## Update CRDs in the opentelemetry-operator-crds subchart
227227
misspell: $(TOOLS_BIN_DIR)/misspell
228228
@echo "running $(MISSPELL)"
229229
@$(MISSPELL) $$($(ALL_SRC_AND_DOC_CMD))
230+
231+
.PHONY: update-matrix-versions
232+
update-matrix-versions: ## Update matrix, ex: K8s cluster versions used for testing. Set DEBUG=-debug to enable debug logs.
233+
go run ./tools/k8s_versions/update_k8s_versions.go $(DEBUG)

tools/k8s_versions/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module k8sVersions
2+
3+
go 1.24.1
4+
5+
require github.com/stretchr/testify v1.10.0
6+
7+
require (
8+
github.com/davecgh/go-spew v1.1.1 // indirect
9+
github.com/pmezard/go-difflib v1.0.0 // indirect
10+
gopkg.in/yaml.v3 v3.0.1 // indirect
11+
)

tools/k8s_versions/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
6+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
// Copyright Splunk Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"encoding/json"
8+
"flag"
9+
"fmt"
10+
"io"
11+
"log"
12+
"net/http"
13+
"os"
14+
"path/filepath"
15+
"regexp"
16+
"sort"
17+
"strconv"
18+
"strings"
19+
"time"
20+
)
21+
22+
var debug bool
23+
24+
const (
25+
EndOfLifeURL string = "https://endoflife.date/api/kubernetes.json"
26+
KindDockerHubURL string = "https://hub.docker.com/v2/repositories/kindest/node/tags?page_size=1&page=1&ordering=last_updated&name="
27+
MiniKubeURL string = "https://raw.githubusercontent.com/kubernetes/minikube/master/pkg/minikube/constants/constants_kubernetes_versions.go"
28+
KubeKindVersion string = "k8s-kind-version"
29+
KubeMinikubeVersion string = "k8s-minikube-version"
30+
)
31+
32+
type KubernetesVersion struct {
33+
Cycle string `json:"cycle"`
34+
ReleaseDate string `json:"releaseDate"`
35+
EOLDate string `json:"eol"`
36+
Latest string `json:"latest"`
37+
}
38+
39+
type DockerImage struct {
40+
Count int `json:"count"`
41+
}
42+
43+
// getSupportedKubernetesVersions returns the supported Kubernetes versions
44+
// by checking the EOL date of the collected versions.
45+
func getSupportedKubernetesVersions(url string) ([]KubernetesVersion, error) {
46+
body, err := getRequestBody(url)
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to get k8s versions: %w", err)
49+
}
50+
var kubernetesVersions, supportedKubernetesVersions []KubernetesVersion
51+
if err = json.Unmarshal(body, &kubernetesVersions); err != nil {
52+
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
53+
}
54+
55+
now := time.Now()
56+
for _, kubernetesVersion := range kubernetesVersions {
57+
eolDate, err := time.Parse(time.DateOnly, kubernetesVersion.EOLDate)
58+
if err != nil {
59+
return nil, fmt.Errorf("error parsing date: %w", err)
60+
}
61+
if eolDate.After(now) {
62+
supportedKubernetesVersions = append(supportedKubernetesVersions, kubernetesVersion)
63+
} else {
64+
logDebug("Skipping version %s, EOL date %s", kubernetesVersion.Cycle, kubernetesVersion.EOLDate)
65+
}
66+
}
67+
return supportedKubernetesVersions, nil
68+
}
69+
70+
// getLatestSupportedMinikubeVersions iterates through the K8s supported versions and find the latest minikube after parsing
71+
// the sorted ValidKubernetesVersions slice from constants_kubernetes_versions.go
72+
func getLatestSupportedMinikubeVersions(url string, k8sVersions []KubernetesVersion) ([]string, error) {
73+
body, err := getRequestBody(url)
74+
if err != nil {
75+
return nil, fmt.Errorf("failed to get minikube versions: %w", err)
76+
}
77+
78+
// Extract the slice using a regular expression
79+
re := regexp.MustCompile(`ValidKubernetesVersions = \[\]string{([^}]*)}`)
80+
matches := re.FindStringSubmatch(string(body))
81+
if len(matches) < 2 {
82+
return nil, fmt.Errorf("minikube, failed to find the Kubernetes versions slice")
83+
}
84+
85+
// Parse and cleanup the slice values
86+
minikubeVersions := strings.Split(strings.NewReplacer("\n", "", `"`, "", "\t", "", " ", "").Replace(matches[1]), ",")
87+
88+
logDebug("Found minikube versions: %s", minikubeVersions)
89+
90+
var latestMinikubeVersions []string
91+
// the minikube version slice is sorted, break when first cycle match is found
92+
for _, k8sVersion := range k8sVersions {
93+
for _, minikubeVersion := range minikubeVersions {
94+
if strings.Contains(minikubeVersion, k8sVersion.Cycle) {
95+
latestMinikubeVersions = append(latestMinikubeVersions, minikubeVersion)
96+
break
97+
}
98+
}
99+
}
100+
101+
return latestMinikubeVersions, nil
102+
}
103+
104+
// getLatestSupportedKindImages iterates through the K8s supported versions and find the latest kind
105+
// tag that supports that version
106+
func getLatestSupportedKindImages(url string, k8sVersions []KubernetesVersion) ([]string, error) {
107+
var supportedKindVersions []string
108+
for _, k8sVersion := range k8sVersions {
109+
tag := k8sVersion.Latest
110+
for {
111+
exists, err := imageTagExists(url, tag)
112+
if err != nil {
113+
return supportedKindVersions, fmt.Errorf("failed to check image tag existence: %w", err)
114+
}
115+
if exists {
116+
supportedKindVersions = append(supportedKindVersions, "v"+tag)
117+
break
118+
}
119+
tag, err = decrementMinorMinorVersion(tag)
120+
if err != nil {
121+
// It's possible that kind still does not have a tag for new versions, break the loop and
122+
// process other k8s versions
123+
if strings.Contains(err.Error(), "minor version cannot be decremented below 0") {
124+
logDebug("No kind image found for k8s version %s", k8sVersion.Cycle)
125+
break
126+
}
127+
return supportedKindVersions, fmt.Errorf("failed to decrement k8sVersion: %w", err)
128+
}
129+
}
130+
}
131+
return supportedKindVersions, nil
132+
}
133+
134+
func imageTagExists(url string, tag string) (bool, error) {
135+
body, err := getRequestBody(url + tag)
136+
if err != nil {
137+
return false, fmt.Errorf("failed to get image tag: %w", err)
138+
}
139+
140+
var kindImage DockerImage
141+
if err := json.Unmarshal(body, &kindImage); err != nil {
142+
return false, fmt.Errorf("failed to unmarshal JSON: %w", err)
143+
}
144+
145+
if kindImage.Count > 0 {
146+
return true, nil
147+
}
148+
return false, nil
149+
}
150+
151+
func decrementMinorMinorVersion(version string) (string, error) {
152+
parts := strings.Split(version, ".")
153+
if len(parts) < 3 {
154+
return "", fmt.Errorf("version does not have a minor version: %s", version)
155+
}
156+
157+
minor, err := strconv.Atoi(parts[2])
158+
if err != nil {
159+
return "", fmt.Errorf("invalid minor version: %s", parts[1])
160+
}
161+
162+
if minor == 0 {
163+
return "", fmt.Errorf("minor version cannot be decremented below 0")
164+
}
165+
166+
parts[2] = strconv.Itoa(minor - 1)
167+
return strings.Join(parts, "."), nil
168+
}
169+
170+
func updateMatrixFile(filePath string, kindVersions []string, minikubeVersions []string) error {
171+
content, err := os.ReadFile(filePath)
172+
if err != nil {
173+
return fmt.Errorf("failed to read file: %w", err)
174+
}
175+
176+
var testMatrix map[string]map[string][]string
177+
if err = json.Unmarshal(content, &testMatrix); err != nil {
178+
return fmt.Errorf("failed to unmarshal JSON: %w", err)
179+
}
180+
181+
for _, value := range testMatrix {
182+
if len(kindVersions) > 0 && value[KubeKindVersion] != nil {
183+
value[KubeKindVersion] = kindVersions
184+
} else if len(minikubeVersions) > 0 && value[KubeMinikubeVersion] != nil {
185+
value[KubeMinikubeVersion] = minikubeVersions
186+
}
187+
}
188+
// Marshal the updated test matrix back to JSON
189+
updatedContent, err := json.MarshalIndent(testMatrix, "", " ")
190+
if err != nil {
191+
return fmt.Errorf("failed to marshal updated JSON: %w", err)
192+
}
193+
194+
// Ensure the file ends with a new line to make the pre-commit check happy
195+
updatedContent = append(updatedContent, '\n')
196+
197+
if err = os.WriteFile(filePath, updatedContent, 0644); err != nil {
198+
return fmt.Errorf("failed to write updated file: %w", err)
199+
}
200+
return nil
201+
}
202+
203+
func sortVersions(versions []string) {
204+
sort.Slice(versions, func(i, j int) bool {
205+
vi := strings.Split(versions[i][1:], ".") // Remove "v" and split by "."
206+
vj := strings.Split(versions[j][1:], ".")
207+
208+
for k := 0; k < len(vi) && k < len(vj); k++ {
209+
if vi[k] != vj[k] {
210+
return vi[k] > vj[k] // Sort in descending order
211+
}
212+
}
213+
return len(vi) > len(vj)
214+
})
215+
}
216+
217+
func getRequestBody(url string) ([]byte, error) {
218+
resp, err := http.Get(url)
219+
if err != nil {
220+
return nil, fmt.Errorf("failed to fetch URL: %w", err)
221+
}
222+
defer resp.Body.Close()
223+
224+
if resp.StatusCode != http.StatusOK {
225+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
226+
}
227+
228+
body, err := io.ReadAll(resp.Body)
229+
if err != nil {
230+
return nil, fmt.Errorf("failed to read response body: %w", err)
231+
}
232+
return body, nil
233+
}
234+
235+
func logDebug(format string, v ...interface{}) {
236+
if debug {
237+
log.Printf(format, v...)
238+
}
239+
}
240+
241+
func main() {
242+
// setup logging
243+
flag.BoolVar(&debug, "debug", false, "Enable debug logging")
244+
flag.Parse()
245+
log.SetOutput(os.Stdout)
246+
log.SetFlags(log.LstdFlags | log.Lshortfile)
247+
248+
k8sVersions, err := getSupportedKubernetesVersions(EndOfLifeURL)
249+
if err != nil || len(k8sVersions) == 0 {
250+
log.Fatalf("Failed to get k8s versions: %v", err)
251+
}
252+
logDebug("Found supported k8s versions %v", k8sVersions)
253+
254+
kindVersions, err := getLatestSupportedKindImages(KindDockerHubURL, k8sVersions)
255+
if err != nil {
256+
log.Printf("failed to get all kind versions: %v", err)
257+
}
258+
if len(kindVersions) > 0 {
259+
// needs to be sorted so we don't end up with false positive diff in the json matrix file
260+
sortVersions(kindVersions)
261+
logDebug("Found supported kind images: %v", kindVersions)
262+
}
263+
264+
minikubeVersions, err := getLatestSupportedMinikubeVersions(MiniKubeURL, k8sVersions)
265+
if err != nil {
266+
log.Printf("failed to get minikube versions: %v", err)
267+
}
268+
if len(minikubeVersions) > 0 {
269+
logDebug("Found supported minikube versions: %v", minikubeVersions)
270+
}
271+
272+
if len(kindVersions) == 0 && len(minikubeVersions) == 0 {
273+
log.Fatalf("No supported versions found. Run with -debug=true for more info.")
274+
}
275+
276+
path := "ci-matrix.json"
277+
currentDir, err := os.Getwd()
278+
if err != nil {
279+
log.Fatalf("Failed to get current directory: %v ", err)
280+
}
281+
path = filepath.Join(currentDir, filepath.Clean(path))
282+
err = updateMatrixFile(path, kindVersions, minikubeVersions)
283+
if err != nil {
284+
log.Fatalf("Failed to update matrix file: %v", err)
285+
}
286+
os.Exit(0)
287+
}

0 commit comments

Comments
 (0)