Skip to content

Commit 7d558b8

Browse files
authored
feat: Add function to get stable kubernetes versions (#1105)
1 parent a7c9794 commit 7d558b8

File tree

6 files changed

+173
-4
lines changed

6 files changed

+173
-4
lines changed

.secrets.baseline

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "go.sum|package-lock.json|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2026-01-23T06:01:17Z",
6+
"generated_at": "2026-01-23T10:01:03Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -192,23 +192,23 @@
192192
"hashed_secret": "892bd503fb45f6fcafb1c7003d88291fc0b20208",
193193
"is_secret": false,
194194
"is_verified": false,
195-
"line_number": 506,
195+
"line_number": 514,
196196
"type": "Secret Keyword",
197197
"verified_result": null
198198
},
199199
{
200200
"hashed_secret": "5da5a31d49370df43eff521b39c10db1466fae44",
201201
"is_secret": false,
202202
"is_verified": false,
203-
"line_number": 512,
203+
"line_number": 520,
204204
"type": "Secret Keyword",
205205
"verified_result": null
206206
},
207207
{
208208
"hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030",
209209
"is_secret": false,
210210
"is_verified": false,
211-
"line_number": 746,
211+
"line_number": 777,
212212
"type": "Secret Keyword",
213213
"verified_result": null
214214
}

cloudinfo/containers.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cloudinfo
33
import (
44
"fmt"
55

6+
"github.com/IBM-Cloud/bluemix-go/api/container/containerv1"
67
"github.com/IBM-Cloud/bluemix-go/api/container/containerv2"
78
)
89

@@ -61,3 +62,47 @@ func (infoSvc *CloudInfoService) GetClusterIngressStatus(clusterId string) (stri
6162
}
6263
return ingressDetails.Status, nil
6364
}
65+
66+
// GetKubeVersions retrieves the available Kubernetes or OpenShift versions
67+
// for a given platform and returns them as a slice of "major.minor" version strings.
68+
//
69+
// KubeVersions().ListV1 returns a map like:
70+
// map[
71+
//
72+
// kubernetes:[{1 31 14 false} {1 32 11 false} {1 33 7 true} {1 34 3 false}]
73+
// openshift:[{4 16 54 false} {4 17 45 false} {4 18 30 false} {4 19 21 true}]
74+
//
75+
// ]
76+
// The function preprocesses this output to return only the versions
77+
// corresponding to the platform passed to GetKubeVersions.
78+
//
79+
// The platform parameter must match a key returned by the API (e.g., "kubernetes" or "openshift").
80+
// This works for both classic and VPC clusters.
81+
func (infoSvc *CloudInfoService) GetKubeVersions(platform string) ([]string, error) {
82+
// Get the container V1 client from the service
83+
containerV1Client := infoSvc.containerV1Client
84+
85+
// Fetch all available cluster versions (kubernetes and openShift) using the V1 API
86+
stableVersions, err := containerV1Client.KubeVersions().ListV1(containerv1.ClusterTargetHeader{})
87+
if err != nil {
88+
return nil, fmt.Errorf("error listing cluster versions: %w", err)
89+
}
90+
91+
if len(stableVersions) == 0 {
92+
return nil, fmt.Errorf("no kubernetes or openShift versions returned")
93+
}
94+
95+
// Get the versions for the requested platform (e.g., "kubernetes" or "openshift")
96+
platformVersions, ok := stableVersions[platform]
97+
if !ok || len(platformVersions) == 0 {
98+
return nil, fmt.Errorf("no versions available for platform: %s", platform)
99+
}
100+
101+
// Convert each KubeVersion struct into a "major.minor" string e.g "4.16"
102+
versions := make([]string, 0, len(platformVersions))
103+
for _, v := range platformVersions {
104+
versions = append(versions, fmt.Sprintf("%d.%d", v.Major, v.Minor))
105+
}
106+
107+
return versions, nil
108+
}

cloudinfo/containers_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,68 @@ func TestGetClusterIngressStatus(t *testing.T) {
136136
}
137137

138138
}
139+
140+
// TestGetKubeVersions validates that GetKubeVersions returns the correct
141+
// major.minor version strings for supported platforms and returns an
142+
// error for unsupported platforms.
143+
func TestGetKubeVersions(t *testing.T) {
144+
mockData := containerv1.V1Version{
145+
"kubernetes": []containerv1.KubeVersion{
146+
{Major: 1, Minor: 31, Patch: 14, Default: false},
147+
{Major: 1, Minor: 33, Patch: 6, Default: true},
148+
},
149+
"openshift": []containerv1.KubeVersion{
150+
{Major: 4, Minor: 16, Patch: 52, Default: false},
151+
{Major: 4, Minor: 19, Patch: 19, Default: true},
152+
},
153+
}
154+
155+
tests := []struct {
156+
name string // Descriptive name of the test case
157+
platform string // Platform passed to GetKubeVersions
158+
expected []string // Expected major.minor versions
159+
expectError bool // Indicates whether an error is expected
160+
}{
161+
{
162+
name: "openshift platform",
163+
platform: "openshift",
164+
expected: []string{"4.16", "4.19"},
165+
},
166+
{
167+
name: "kubernetes platform",
168+
platform: "kubernetes",
169+
expected: []string{"1.31", "1.33"},
170+
},
171+
{
172+
name: "invalid platform",
173+
platform: "invalid",
174+
expectError: true,
175+
},
176+
}
177+
178+
for _, tt := range tests {
179+
t.Run(tt.name, func(t *testing.T) {
180+
mockContainerV1Client := &containerV1ClientMock{}
181+
mockKubeVersions := &KubeVersionsMock{}
182+
183+
mockContainerV1Client.On("KubeVersions").Return(mockKubeVersions)
184+
mockKubeVersions.
185+
On("ListV1", containerv1.ClusterTargetHeader{}).
186+
Return(mockData, nil)
187+
188+
infoSvc := CloudInfoService{
189+
containerV1Client: mockContainerV1Client,
190+
}
191+
192+
versions, err := infoSvc.GetKubeVersions(tt.platform)
193+
194+
if tt.expectError {
195+
assert.Error(t, err)
196+
assert.Nil(t, versions)
197+
} else {
198+
assert.NoError(t, err)
199+
assert.Equal(t, tt.expected, versions)
200+
}
201+
})
202+
}
203+
}

cloudinfo/mock_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,27 @@ func (s *icdServiceMock) NewListDeployablesOptions() *clouddatabasesv5.ListDeplo
431431
func (s *icdServiceMock) ListDeployables(*clouddatabasesv5.ListDeployablesOptions) (*clouddatabasesv5.ListDeployablesResponse, *core.DetailedResponse, error) {
432432
return s.mockListDeployablesResponse, nil, nil
433433
}
434+
435+
// Mock ContainerV1 Client
436+
type containerV1ClientMock struct {
437+
mock.Mock
438+
}
439+
440+
func (mock *containerV1ClientMock) KubeVersions() containerv1.KubeVersions {
441+
args := mock.Called()
442+
return args.Get(0).(containerv1.KubeVersions) // Cast to the expected return type
443+
}
444+
445+
type KubeVersionsMock struct {
446+
mock.Mock
447+
}
448+
449+
func (m *KubeVersionsMock) List(target containerv1.ClusterTargetHeader) ([]containerv1.KubeVersion, error) {
450+
args := m.Called(target)
451+
return args.Get(0).([]containerv1.KubeVersion), args.Error(1)
452+
}
453+
454+
func (m *KubeVersionsMock) ListV1(target containerv1.ClusterTargetHeader) (containerv1.V1Version, error) {
455+
args := m.Called(target)
456+
return args.Get(0).(containerv1.V1Version), args.Error(1)
457+
}

cloudinfo/service.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
projects "github.com/IBM/project-go-sdk/projectv1"
1919

2020
"github.com/IBM-Cloud/bluemix-go"
21+
"github.com/IBM-Cloud/bluemix-go/api/container/containerv1"
2122
"github.com/IBM-Cloud/bluemix-go/api/container/containerv2"
2223
"github.com/IBM-Cloud/bluemix-go/session"
2324
ibmpimodels "github.com/IBM-Cloud/power-go-client/power/models"
@@ -152,6 +153,7 @@ type CloudInfoService struct {
152153
resourceManagerService resourceManagerService
153154
cbrService cbrService
154155
containerClient containerClient
156+
containerV1Client containerV1Client
155157
catalogService catalogService
156158
// stackDefinitionCreator is used to create stack definitions and only added to support testing/mocking
157159
stackDefinitionCreator StackDefinitionCreator
@@ -282,6 +284,7 @@ type CloudInfoServiceOptions struct {
282284
IamPolicyService iamPolicyService
283285
CbrService cbrService
284286
ContainerClient containerClient
287+
ContainerV1Client containerV1Client
285288
RegionPrefs []RegionData
286289
IcdService icdService
287290
IcdRegion string
@@ -359,6 +362,11 @@ type containerClient interface {
359362
Albs() containerv2.Alb
360363
}
361364

365+
// containerV1Client interface for external Kubernetes Versions Service API. Used for mocking.
366+
type containerV1Client interface {
367+
KubeVersions() containerv1.KubeVersions
368+
}
369+
362370
// cbrService interface for external Context Based Restrictions Service API. Used for mocking.
363371
type cbrService interface {
364372
GetRule(*contextbasedrestrictionsv1.GetRuleOptions) (*contextbasedrestrictionsv1.Rule, *core.DetailedResponse, error)
@@ -598,6 +606,29 @@ func NewCloudInfoServiceWithKey(options CloudInfoServiceOptions) (*CloudInfoServ
598606
}
599607
infoSvc.containerClient = containerClient
600608
}
609+
610+
// if containerV1Client is not supplied, use default external service
611+
if options.ContainerV1Client != nil {
612+
infoSvc.containerV1Client = options.ContainerV1Client
613+
} else {
614+
// Create a new Bluemix session
615+
sess, sessErr := session.New(&bluemix.Config{
616+
BluemixAPIKey: infoSvc.ApiKey, // pragma: allowlist secret
617+
})
618+
if sessErr != nil {
619+
log.Println("ERROR: Could not create Bluemix session:", sessErr)
620+
return nil, sessErr
621+
}
622+
623+
// Initialize the containerv1 service client with the session
624+
containerV1Client, containerErr := containerv1.New(sess)
625+
if containerErr != nil {
626+
log.Println("ERROR: Could not create containerv1 service client:", containerErr)
627+
return nil, containerErr
628+
}
629+
infoSvc.containerV1Client = containerV1Client
630+
}
631+
601632
// if resourceControllerService is not supplied use new external
602633
if options.ResourceControllerService != nil {
603634
infoSvc.resourceControllerService = options.ResourceControllerService

cloudinfo/service_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func TestNewServiceWithKey(t *testing.T) {
1515
IamPolicyService: new(iamPolicyServiceMock),
1616
ResourceControllerService: new(resourceControllerServiceMock),
1717
ContainerClient: new(containerClientMock),
18+
ContainerV1Client: new(containerV1ClientMock),
1819
}
1920

2021
_, err := NewCloudInfoServiceWithKey(serviceOptions)
@@ -29,6 +30,7 @@ func TestNewServiceWithEnv(t *testing.T) {
2930
IamPolicyService: new(iamPolicyServiceMock),
3031
ResourceControllerService: new(resourceControllerServiceMock),
3132
ContainerClient: new(containerClientMock),
33+
ContainerV1Client: new(containerV1ClientMock),
3234
}
3335

3436
if err := os.Setenv("TEST_KEY_VAL", "dummy_key"); err != nil {
@@ -47,6 +49,7 @@ func TestNewServiceWithEmptyKey(t *testing.T) {
4749
IamPolicyService: new(iamPolicyServiceMock),
4850
ResourceControllerService: new(resourceControllerServiceMock),
4951
ContainerClient: new(containerClientMock),
52+
ContainerV1Client: new(containerV1ClientMock),
5053
}
5154

5255
_, err := NewCloudInfoServiceWithKey(serviceOptions)
@@ -61,6 +64,7 @@ func TestNewServiceWithEmptyEnv(t *testing.T) {
6164
IamPolicyService: new(iamPolicyServiceMock),
6265
ResourceControllerService: new(resourceControllerServiceMock),
6366
ContainerClient: new(containerClientMock),
67+
ContainerV1Client: new(containerV1ClientMock),
6468
}
6569

6670
_, err := NewCloudInfoServiceFromEnv("", serviceOptions)

0 commit comments

Comments
 (0)