Skip to content

Commit 3e8e5ec

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

File tree

6 files changed

+468
-0
lines changed

6 files changed

+468
-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
@@ -221,3 +221,7 @@ tidy-all:
221221
.PHONY: update-operator-crds
222222
update-operator-crds: ## Update CRDs in the opentelemetry-operator-crds subchart
223223
ci_scripts/update-crds.sh
224+
225+
.PHONY: update-matrix-versions
226+
update-matrix-versions: ## Update matrix, ex: K8s cluster versions used for testing. Set DEBUG=-debug to enable debug logs.
227+
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: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
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+
)
29+
30+
type KubernetesVersion struct {
31+
Cycle string `json:"cycle"`
32+
ReleaseDate string `json:"releaseDate"`
33+
EOLDate string `json:"eol"`
34+
Latest string `json:"latest"`
35+
}
36+
37+
type DockerImage struct {
38+
Count int `json:"count"`
39+
}
40+
41+
// getSupportedKubernetesVersions returns the supported Kubernetes versions
42+
// by checking the EOL date of the collected versions.
43+
func getSupportedKubernetesVersions(url string) ([]KubernetesVersion, error) {
44+
body, err := getRequestBody(url)
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to get k8s versions: %w", err)
47+
}
48+
var kubernetesVersions, supportedKubernetesVersions []KubernetesVersion
49+
if err := json.Unmarshal(body, &kubernetesVersions); err != nil {
50+
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
51+
}
52+
53+
now := time.Now()
54+
for _, kubernetesVersion := range kubernetesVersions {
55+
eolDate, err := time.Parse("2006-01-02", kubernetesVersion.EOLDate)
56+
if err != nil {
57+
return nil, fmt.Errorf("error parsing date: %w", err)
58+
}
59+
if eolDate.After(now) {
60+
supportedKubernetesVersions = append(supportedKubernetesVersions, kubernetesVersion)
61+
} else {
62+
logDebug("Skipping version %s, EOL date %s", kubernetesVersion.Cycle, kubernetesVersion.EOLDate)
63+
}
64+
}
65+
return supportedKubernetesVersions, nil
66+
}
67+
68+
// getLatestSupportedMinikubeVersions iterates through the K8s supported versions and find the latest minikube after parsing
69+
// the sorted ValidKubernetesVersions slice from constants_kubernetes_versions.go
70+
func getLatestSupportedMinikubeVersions(url string, k8sVersions []KubernetesVersion) ([]string, error) {
71+
body, err := getRequestBody(url)
72+
if err != nil {
73+
return nil, fmt.Errorf("failed to get minikube versions: %w", err)
74+
}
75+
76+
// Extract the slice using a regular expression
77+
re := regexp.MustCompile(`ValidKubernetesVersions = \[\]string{([^}]*)}`)
78+
matches := re.FindStringSubmatch(string(body))
79+
if len(matches) < 2 {
80+
return nil, fmt.Errorf("minikube, failed to find the Kubernetes versions slice")
81+
}
82+
83+
// Parse and cleanup the slice values
84+
minikuveVersions := strings.Split(strings.ReplaceAll(strings.ReplaceAll(matches[1], "\n", ""), `"`, ""), ",")
85+
86+
logDebug("Found minikube versions: %s", minikuveVersions)
87+
88+
var latestMinikubeVersions []string
89+
// the minikube version slice is sorted, break when first cycle match is found
90+
for _, k8sVersion := range k8sVersions {
91+
for _, minikubeVersion := range minikuveVersions {
92+
if strings.Contains(minikubeVersion, k8sVersion.Cycle) {
93+
latestMinikubeVersions = append(latestMinikubeVersions, strings.TrimSpace(minikubeVersion))
94+
break
95+
}
96+
}
97+
}
98+
99+
return latestMinikubeVersions, nil
100+
}
101+
102+
// getLatestSupportedKindImages iterates through the K8s supported versions and find the latest kind
103+
// tag that supports that version
104+
func getLatestSupportedKindImages(url string, k8sVersions []KubernetesVersion) ([]string, error) {
105+
var supportedKindVersions []string
106+
for _, k8sVersion := range k8sVersions {
107+
tag := k8sVersion.Latest
108+
for {
109+
exists, err := imageTagExists(url, tag)
110+
if err != nil {
111+
return supportedKindVersions, fmt.Errorf("failed to check image tag existence: %w", err)
112+
}
113+
if exists {
114+
supportedKindVersions = append(supportedKindVersions, "v"+tag)
115+
break
116+
}
117+
tag, err = decrementMinorMinorVersion(tag)
118+
if err != nil {
119+
// It's possible that kind still does not have a tag for new versions, break the loop and
120+
// process other k8s versions
121+
if strings.Contains(err.Error(), "minor version cannot be decremented below 0") {
122+
logDebug("No kind image found for k8s version %s", k8sVersion.Cycle)
123+
break
124+
}
125+
return supportedKindVersions, fmt.Errorf("failed to decrement k8sVersion: %w", err)
126+
}
127+
}
128+
}
129+
return supportedKindVersions, nil
130+
}
131+
132+
func imageTagExists(url string, tag string) (bool, error) {
133+
body, err := getRequestBody(url + tag)
134+
if err != nil {
135+
return false, fmt.Errorf("failed to get image tag: %w", err)
136+
}
137+
138+
var kindImage DockerImage
139+
if err := json.Unmarshal(body, &kindImage); err != nil {
140+
return false, fmt.Errorf("failed to unmarshal JSON: %w", err)
141+
}
142+
143+
if kindImage.Count > 0 {
144+
return true, nil
145+
}
146+
return false, nil
147+
}
148+
149+
func decrementMinorMinorVersion(version string) (string, error) {
150+
parts := strings.Split(version, ".")
151+
if len(parts) < 3 {
152+
return "", fmt.Errorf("version does not have a minor version: %s", version)
153+
}
154+
155+
minor, err := strconv.Atoi(parts[2])
156+
if err != nil {
157+
return "", fmt.Errorf("invalid minor version: %s", parts[1])
158+
}
159+
160+
if minor == 0 {
161+
return "", fmt.Errorf("minor version cannot be decremented below 0")
162+
}
163+
164+
parts[2] = strconv.Itoa(minor - 1)
165+
return strings.Join(parts, "."), nil
166+
}
167+
168+
func updateMatrixFile(filePath string, kindVersions []string, minikubeVersions []string) error {
169+
content, err := os.ReadFile(filePath)
170+
if err != nil {
171+
return fmt.Errorf("failed to read file: %w", err)
172+
}
173+
174+
var testMatrix map[string]map[string][]string
175+
if err := json.Unmarshal(content, &testMatrix); err != nil {
176+
return fmt.Errorf("failed to unmarshal JSON: %w", err)
177+
}
178+
179+
for _, value := range testMatrix {
180+
if len(kindVersions) > 0 && value["k8s-kind-version"] != nil {
181+
value["k8s-kind-version"] = kindVersions
182+
} else if len(minikubeVersions) > 0 && value["k8s-minikube-version"] != nil {
183+
value["k8s-minikube-version"] = minikubeVersions
184+
}
185+
}
186+
// Marshal the updated test matrix back to JSON
187+
updatedContent, err := json.MarshalIndent(testMatrix, "", " ")
188+
if err != nil {
189+
return fmt.Errorf("failed to marshal updated JSON: %w", err)
190+
}
191+
192+
// Ensure the file ends with a new line to make the pre-commit check happy
193+
updatedContent = append(updatedContent, '\n')
194+
195+
if err := os.WriteFile(filePath, updatedContent, 0644); err != nil {
196+
return fmt.Errorf("failed to write updated file: %w", err)
197+
}
198+
return nil
199+
}
200+
201+
func sortVersions(versions []string) {
202+
sort.Slice(versions, func(i, j int) bool {
203+
vi := strings.Split(versions[i][1:], ".") // Remove "v" and split by "."
204+
vj := strings.Split(versions[j][1:], ".")
205+
206+
for k := 0; k < len(vi) && k < len(vj); k++ {
207+
if vi[k] != vj[k] {
208+
return vi[k] > vj[k] // Sort in descending order
209+
}
210+
}
211+
return len(vi) > len(vj)
212+
})
213+
}
214+
215+
func getRequestBody(url string) ([]byte, error) {
216+
resp, err := http.Get(url)
217+
if err != nil {
218+
return nil, fmt.Errorf("failed to fetch URL: %w", err)
219+
}
220+
defer resp.Body.Close()
221+
222+
if resp.StatusCode != http.StatusOK {
223+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
224+
}
225+
226+
body, err := io.ReadAll(resp.Body)
227+
if err != nil {
228+
return nil, fmt.Errorf("failed to read response body: %w", err)
229+
}
230+
return body, nil
231+
}
232+
233+
func logDebug(format string, v ...interface{}) {
234+
if debug {
235+
log.Printf(format, v...)
236+
}
237+
}
238+
239+
func main() {
240+
// setup logging
241+
flag.BoolVar(&debug, "debug", false, "Enable debug logging")
242+
flag.Parse()
243+
log.SetOutput(os.Stdout)
244+
log.SetFlags(log.LstdFlags | log.Lshortfile)
245+
246+
k8sVersions, err := getSupportedKubernetesVersions(EndOfLifeURL)
247+
if err != nil || len(k8sVersions) == 0 {
248+
log.Fatalf("Failed to get k8s versions: %v", err)
249+
}
250+
logDebug("Found supported k8s versions %v", k8sVersions)
251+
252+
kindVersions, err := getLatestSupportedKindImages(KindDockerHubURL, k8sVersions)
253+
if err != nil {
254+
log.Printf("failed to get all kind versions: %v", err)
255+
}
256+
if len(kindVersions) > 0 {
257+
// needs to be sorted so we don't end up with false positive diff in the json matrix file
258+
sortVersions(kindVersions)
259+
logDebug("Found supported kind images: %v", kindVersions)
260+
}
261+
262+
minikubeVersions, err := getLatestSupportedMinikubeVersions(MiniKubeURL, k8sVersions)
263+
if err != nil {
264+
log.Printf("failed to get minikube versions: %v", err)
265+
}
266+
if len(minikubeVersions) > 0 {
267+
logDebug("Found supported minikube versions: %v", minikubeVersions)
268+
}
269+
270+
if len(kindVersions) == 0 && len(minikubeVersions) == 0 {
271+
log.Fatalf("No supported versions found. Run with -debug=true for more info.")
272+
}
273+
274+
path := "ci-matrix.json"
275+
currentDir, err := os.Getwd()
276+
if err != nil {
277+
log.Fatalf("Failed to get current directory: %v ", err)
278+
}
279+
path = filepath.Join(currentDir, filepath.Clean(path))
280+
err = updateMatrixFile(path, kindVersions, minikubeVersions)
281+
if err != nil {
282+
log.Fatalf("Failed to update matrix file: %v", err)
283+
}
284+
os.Exit(0)
285+
}

0 commit comments

Comments
 (0)