Skip to content

Commit 6a6ba28

Browse files
authored
[chore] add logic to update our tests k8s versions (#1792)
* [chore] add logic to update our tests k8s versions Signed-off-by: Dani Louca <dlouca@splunk.com> * conflict Signed-off-by: Dani Louca <dlouca@splunk.com> --------- Signed-off-by: Dani Louca <dlouca@splunk.com>
1 parent 968ce08 commit 6a6ba28

File tree

7 files changed

+511
-0
lines changed

7 files changed

+511
-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
@@ -257,3 +257,7 @@ gogci-all:
257257
.PHONY: gomoddownload
258258
gomoddownload:
259259
@$(MAKE) for-all-target TARGET="moddownload"
260+
261+
.PHONY: update-matrix-versions
262+
update-matrix-versions: ## Update matrix, ex: K8s cluster versions used for testing. Set DEBUG=-debug to enable debug logs.
263+
go run ./tools/k8s_versions/update_k8s_versions.go $(DEBUG)

tools/k8s_versions/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include ../../Makefile.common

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

0 commit comments

Comments
 (0)