Skip to content

Commit 91ee5c4

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

File tree

4 files changed

+354
-0
lines changed

4 files changed

+354
-0
lines changed

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-k8s-test-versions
226+
update-k8s-test-versions: ## Update K8S cluster versions used for testing
227+
go run ./tools/k8s_versions/update_k8s_versions.go

tools/k8s_versions/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module k8sVersions
2+
3+
go 1.24.1
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Copyright Splunk Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"bufio"
8+
"encoding/json"
9+
"fmt"
10+
"io"
11+
"net/http"
12+
"os"
13+
"path/filepath"
14+
"regexp"
15+
"strconv"
16+
"strings"
17+
"time"
18+
)
19+
20+
type KubernetesVersion struct {
21+
Cycle string `json:"cycle"`
22+
ReleaseDate string `json:"releaseDate"`
23+
EOLDate string `json:"eol"`
24+
Latest string `json:"latest"`
25+
}
26+
27+
type DockerImage struct {
28+
Count int `json:"count"`
29+
}
30+
31+
func fetchKubernetesVersions(url string) ([]KubernetesVersion, error) {
32+
resp, err := http.Get(url)
33+
if err != nil {
34+
return nil, fmt.Errorf("failed getting k8s versions: %w", err)
35+
}
36+
defer resp.Body.Close()
37+
38+
if resp.StatusCode != http.StatusOK {
39+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
40+
}
41+
42+
body, err := io.ReadAll(resp.Body)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to read response body: %w", err)
45+
}
46+
47+
var kubernetesVersions []KubernetesVersion
48+
if err := json.Unmarshal(body, &kubernetesVersions); err != nil {
49+
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
50+
}
51+
52+
return kubernetesVersions, nil
53+
}
54+
55+
// yaml parsing does not preserve the original format, mainly blank lines.
56+
// This function reads the raw yaml file, locate and update the k8s-version block
57+
func updateK8sVersion(filePath string, kubernetesVersions []KubernetesVersion) error {
58+
file, err := os.Open(filePath)
59+
if err != nil {
60+
return fmt.Errorf("failed to open file: %w", err)
61+
}
62+
defer file.Close()
63+
64+
var output []string
65+
var k8sVersionBlock []string
66+
var inK8sVersionBlock bool
67+
68+
scanner := bufio.NewScanner(file)
69+
for scanner.Scan() {
70+
line := scanner.Text()
71+
trimmed := strings.TrimSpace(line)
72+
73+
// Locate the k8s-version block needed to update
74+
if strings.HasPrefix(trimmed, "k8s-version:") || strings.HasPrefix(trimmed, "kubernetes_version:") {
75+
inK8sVersionBlock = true
76+
output = append(output, line)
77+
continue
78+
}
79+
// if we found the k8s version array, extract all versions and pass them to updateVersionsBlock
80+
if inK8sVersionBlock {
81+
if strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "#") {
82+
k8sVersionBlock = append(k8sVersionBlock, line)
83+
continue
84+
} else if trimmed == "" || !strings.HasPrefix(trimmed, "-") {
85+
updatedVerBlock, err := updateVersionsBlock(kubernetesVersions, k8sVersionBlock)
86+
if err != nil {
87+
return err
88+
}
89+
output = append(output, append(updatedVerBlock, line)...)
90+
inK8sVersionBlock = false
91+
continue
92+
}
93+
}
94+
output = append(output, line)
95+
}
96+
97+
if err := scanner.Err(); err != nil {
98+
return fmt.Errorf("failed to read file: %w", err)
99+
}
100+
101+
if err := os.WriteFile(filePath, []byte(strings.Join(output, "\n")+"\n"), 0644); err != nil {
102+
return fmt.Errorf("failed to write updated file: %w", err)
103+
}
104+
return nil
105+
}
106+
107+
func updateVersionsBlock(versions []KubernetesVersion, versionBlock []string) ([]string, error) {
108+
var output []string
109+
now := time.Now()
110+
const layout = "2006-01-02"
111+
versionRE := regexp.MustCompile("\\d+.\\d+.\\d+")
112+
113+
for _, version := range versions {
114+
eolDate, err := time.Parse(layout, version.EOLDate)
115+
if err != nil {
116+
return nil, fmt.Errorf("error parsing date: %w", err)
117+
}
118+
119+
for i, line := range versionBlock {
120+
trimmed := strings.TrimSpace(line)
121+
tag := ""
122+
if strings.HasPrefix(trimmed, fmt.Sprintf("- v%s", version.Cycle)) {
123+
if eolDate.After(now) {
124+
tag, err = getAvailableKindImage(version.Latest)
125+
if err != nil {
126+
return nil, fmt.Errorf("failed to get available kind image: %w", err)
127+
}
128+
line = versionRE.ReplaceAllString(line, tag)
129+
output = append(output, line)
130+
}
131+
break
132+
} else if i == len(versionBlock)-1 && eolDate.After(now) {
133+
// Add the new version at the end of the list
134+
tag, err = getAvailableKindImage(version.Latest)
135+
if err != nil {
136+
return nil, fmt.Errorf("failed to get available kind image: %w", err)
137+
}
138+
output = append(output, fmt.Sprintf("%s- v%s # EOL %s", strings.Repeat(" ", len(line)-len(trimmed)), tag, version.EOLDate))
139+
}
140+
}
141+
}
142+
return output, nil
143+
}
144+
145+
func getAvailableKindImage(tag string) (string, error) {
146+
for {
147+
exists, err := imageTagExists(tag)
148+
if err != nil {
149+
return "", fmt.Errorf("failed to check image tag existence: %w", err)
150+
}
151+
if exists {
152+
return tag, nil
153+
}
154+
155+
tag, err = decrementMinorMinorVersion(tag)
156+
if err != nil {
157+
return "", fmt.Errorf("failed to decrement version: %w", err)
158+
}
159+
}
160+
}
161+
162+
func imageTagExists(tag string) (bool, error) {
163+
url := "https://hub.docker.com/v2/repositories/kindest/node/tags?page_size=1&page=1&ordering=last_updated&name="
164+
resp, err := http.Get(url + tag)
165+
if err != nil {
166+
return false, fmt.Errorf("failed getting k8s versions: %w", err)
167+
}
168+
defer resp.Body.Close()
169+
170+
if resp.StatusCode != http.StatusOK {
171+
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
172+
}
173+
174+
body, err := io.ReadAll(resp.Body)
175+
if err != nil {
176+
return false, fmt.Errorf("failed to read response body: %w", err)
177+
}
178+
179+
var kindImage DockerImage
180+
if err := json.Unmarshal(body, &kindImage); err != nil {
181+
return false, fmt.Errorf("failed to unmarshal JSON: %w", err)
182+
}
183+
184+
if kindImage.Count > 0 {
185+
return true, nil
186+
}
187+
return false, nil
188+
}
189+
190+
func decrementMinorMinorVersion(version string) (string, error) {
191+
parts := strings.Split(version, ".")
192+
if len(parts) < 3 {
193+
return "", fmt.Errorf("version does not have a minor version: %s", version)
194+
}
195+
196+
minor, err := strconv.Atoi(parts[2])
197+
if err != nil {
198+
return "", fmt.Errorf("invalid minor version: %s", parts[1])
199+
}
200+
201+
if minor == 0 {
202+
return "", fmt.Errorf("minor version cannot be decremented below 0")
203+
}
204+
205+
parts[2] = strconv.Itoa(minor - 1)
206+
return strings.Join(parts, "."), nil
207+
}
208+
209+
func main() {
210+
url := "https://endoflife.date/api/kubernetes.json"
211+
k8sVersions, err := fetchKubernetesVersions(url)
212+
if err != nil {
213+
fmt.Println("Failed to get k8s versions:", err)
214+
os.Exit(1) // Exit with code 1 on failure
215+
}
216+
217+
filesToUpdate := []string{".github/workflows/functional_test.yaml", ".github/workflows/functional_test_v2.yaml"}
218+
currentDir, err := os.Getwd()
219+
if err != nil {
220+
fmt.Println("Failed to get current directory:", err)
221+
os.Exit(1)
222+
}
223+
224+
var partialFailure bool
225+
for _, filePath := range filesToUpdate {
226+
filePath = filepath.Join(currentDir, filepath.Clean(filePath))
227+
if err := updateK8sVersion(filePath, k8sVersions); err != nil {
228+
fmt.Println("Error:", err)
229+
partialFailure = true
230+
}
231+
}
232+
233+
if partialFailure {
234+
os.Exit(2)
235+
}
236+
237+
os.Exit(0)
238+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"os"
7+
"strings"
8+
"testing"
9+
)
10+
11+
func TestFetchKubernetesVersions_ValidURL_ReturnsData(t *testing.T) {
12+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
w.WriteHeader(http.StatusOK)
14+
w.Write([]byte(`[{"cycle":"v1.24","releaseDate":"2022-05-03","eol":"2023-10-03","latest":"v1.24.0"}]`))
15+
}))
16+
defer mockServer.Close()
17+
18+
versions, err := fetchKubernetesVersions(mockServer.URL)
19+
if err != nil {
20+
t.Fatalf("Expected no error, got %v", err)
21+
}
22+
if len(versions) != 1 {
23+
t.Fatalf("Expected 1 version, got %d", len(versions))
24+
}
25+
if versions[0].Cycle != "v1.24" {
26+
t.Fatalf("Expected cycle v1.24, got %s", versions[0].Cycle)
27+
}
28+
}
29+
30+
func TestFetchKubernetesVersions_InvalidURL_ReturnsError(t *testing.T) {
31+
url := "https://invalid.url"
32+
_, err := fetchKubernetesVersions(url)
33+
if err == nil {
34+
t.Fatal("Expected an error, got nil")
35+
}
36+
}
37+
38+
func TestUpdateVersionsBlock_ValidInput_UpdatesBlock(t *testing.T) {
39+
versions := []KubernetesVersion{
40+
{"v1.24", "2022-05-03", "2034-10-03", "v1.24.0"},
41+
{"v1.25", "2022-08-02", "2036-01-02", "v1.25.0"},
42+
}
43+
versionBlock := []string{
44+
"- v1.22 # EOL 2023-10-03",
45+
"- v1.23 # EOL 2023-01-01",
46+
}
47+
result, err := updateVersionsBlock(versions, versionBlock)
48+
if err != nil {
49+
t.Fatalf("Expected no error, got %v", err)
50+
}
51+
if len(result) != 2 {
52+
t.Fatalf("Expected 2 lines, got %d", len(result))
53+
}
54+
}
55+
56+
func TestUpdateVersionsBlock_EmptyInput_ReturnsEmpty(t *testing.T) {
57+
versions := []KubernetesVersion{}
58+
versionBlock := []string{}
59+
result, err := updateVersionsBlock(versions, versionBlock)
60+
if err != nil {
61+
t.Fatalf("Expected no error, got %v", err)
62+
}
63+
if len(result) != 0 {
64+
t.Fatalf("Expected empty result, got %v", result)
65+
}
66+
}
67+
68+
func TestUpdateK8sVersion_ValidFile_UpdatesFile(t *testing.T) {
69+
filePath := "test.yaml"
70+
_ = os.WriteFile(filePath, []byte("k8s-version:\n- v1.23 # EOL 2023-01-01\n\n"), 0644)
71+
defer os.Remove(filePath)
72+
73+
versions := []KubernetesVersion{
74+
{"v1.24", "2022-05-03", "2033-10-03", "v1.24.0"},
75+
}
76+
77+
err := updateK8sVersion(filePath, versions)
78+
if err != nil {
79+
t.Fatalf("Expected no error, got %v", err)
80+
}
81+
82+
content, _ := os.ReadFile(filePath)
83+
if !strings.Contains(string(content), "v1.24") {
84+
t.Fatalf("Expected updated content with v1.24, got %s", string(content))
85+
}
86+
}
87+
88+
func TestUpdateK8sVersion_InvalidFile_ReturnsError(t *testing.T) {
89+
err := updateK8sVersion("nonexistent.yaml", nil)
90+
if err == nil {
91+
t.Fatal("Expected an error, got nil")
92+
}
93+
}
94+
95+
func TestUpdateVersionsBlock_EOLDateInPast_ExcludesVersion(t *testing.T) {
96+
versions := []KubernetesVersion{
97+
{"v1.24", "2022-05-03", "2020-01-01", "v1.24.0"},
98+
}
99+
versionBlock := []string{
100+
"- v1.24 # EOL 2020-01-01",
101+
}
102+
result, err := updateVersionsBlock(versions, versionBlock)
103+
if err != nil {
104+
t.Fatalf("Expected no error, got %v", err)
105+
}
106+
if len(result) != 0 {
107+
t.Fatalf("Expected empty result, got %v", result)
108+
}
109+
}

0 commit comments

Comments
 (0)