Skip to content

Commit e7b9030

Browse files
laveryasgalsaleh
andauthored
use EC kinds for the EC node join response, and include the app version (#5274)
* use EC join command response type from EC kinds * return kots app version label if the app is installed * update types again * update with unified kinds * cleanup imports * add CurrentSequence comment * Update pkg/handlers/embedded_cluster_node_join_command.go * return error if fail to get apps --------- Co-authored-by: Salah Al Saleh <[email protected]>
1 parent f31ff2c commit e7b9030

File tree

4 files changed

+143
-26
lines changed

4 files changed

+143
-26
lines changed

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/replicatedhq/kots
22

3-
go 1.24.0
3+
go 1.24.1
44

55
require (
66
cloud.google.com/go/storage v1.45.0
@@ -49,7 +49,7 @@ require (
4949
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
5050
github.com/pkg/errors v0.9.1
5151
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
52-
github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20250411154749-d20d2f980f0c
52+
github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20250415224730-0f6eb6643335
5353
github.com/replicatedhq/kotskinds v0.0.0-20250411153224-089dbeb7ba2a
5454
github.com/replicatedhq/kurlkinds v1.5.0
5555
github.com/replicatedhq/troubleshoot v0.117.0
@@ -414,6 +414,7 @@ require (
414414
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
415415
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
416416
github.com/hashicorp/logutils v1.0.0 // indirect
417+
github.com/k0sproject/dig v0.2.0 // indirect
417418
github.com/kubernetes-csi/external-snapshotter/client/v7 v7.0.0 // indirect
418419
github.com/miekg/dns v1.1.63 // indirect
419420
github.com/moby/docker-image-spec v1.3.1 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
15451545
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
15461546
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
15471547
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
1548+
github.com/k0sproject/dig v0.2.0 h1:cNxEIl96g9kqSMfPSZLhpnZ0P8bWXKv08nxvsMHop5w=
1549+
github.com/k0sproject/dig v0.2.0/go.mod h1:rBcqaQlJpcKdt2x/OE/lPvhGU50u/e95CSm5g/r4s78=
15481550
github.com/k0sproject/k0s v1.30.10-0.20250117153350-dcf3c22bb568 h1:JSfvTBrsNMWDISDUMVRZV6hP5eRusBS6d0Gv2lA4lSA=
15491551
github.com/k0sproject/k0s v1.30.10-0.20250117153350-dcf3c22bb568/go.mod h1:Nmj+slwFht6ile7OHHGiSrcRRGmrA9U9PzjnG9/6gc0=
15501552
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
@@ -1861,8 +1863,8 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0
18611863
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
18621864
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
18631865
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
1864-
github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20250411154749-d20d2f980f0c h1:QoAn+gFKypZPuybDUIzYQt/hzSLOMwTab/csazCAZQ8=
1865-
github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20250411154749-d20d2f980f0c/go.mod h1:DZVH5BSrkKaZPYO6psQYRZgzPdwaxrh8CpJYW95D76E=
1866+
github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20250415224730-0f6eb6643335 h1:PHC8qBJWV7+gfj1icO6dcM3eU5ofURdpcRdr2aj6sDw=
1867+
github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20250415224730-0f6eb6643335/go.mod h1:+f76CfnrG1XqQyRvRaJ0+xMkRP9uvlWjx4hFySfnfnw=
18661868
github.com/replicatedhq/kotskinds v0.0.0-20250411153224-089dbeb7ba2a h1:aNZ7qcuEmPGIUIIfxF7c0sdKR2+zL2vc5r2V8j8a49I=
18671869
github.com/replicatedhq/kotskinds v0.0.0-20250411153224-089dbeb7ba2a/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I=
18681870
github.com/replicatedhq/kurlkinds v1.5.0 h1:zZ0PKNeh4kXvSzVGkn62DKTo314GxhXg1TSB3azURMc=

pkg/handlers/embedded_cluster_node_join_command.go

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import (
55
"fmt"
66
"net/http"
77

8-
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
8+
"github.com/google/uuid"
9+
"github.com/replicatedhq/embedded-cluster/kinds/types/join"
910

1011
"github.com/replicatedhq/kots/pkg/embeddedcluster"
1112
"github.com/replicatedhq/kots/pkg/k8sutil"
@@ -19,16 +20,6 @@ type GenerateEmbeddedClusterNodeJoinCommandResponse struct {
1920
Command []string `json:"command"`
2021
}
2122

22-
type GetEmbeddedClusterNodeJoinCommandResponse struct {
23-
ClusterID string `json:"clusterID"`
24-
K0sJoinCommand string `json:"k0sJoinCommand"`
25-
K0sToken string `json:"k0sToken"`
26-
EmbeddedClusterVersion string `json:"embeddedClusterVersion"`
27-
AirgapRegistryAddress string `json:"airgapRegistryAddress"`
28-
TCPConnectionsRequired []string `json:"tcpConnectionsRequired"`
29-
InstallationSpec ecv1beta1.InstallationSpec `json:"installationSpec,omitempty"`
30-
}
31-
3223
type GenerateEmbeddedClusterNodeJoinCommandRequest struct {
3324
Roles []string `json:"roles"`
3425
}
@@ -178,13 +169,45 @@ func (h *Handler) GetEmbeddedClusterNodeJoinCommand(w http.ResponseWriter, r *ht
178169
return
179170
}
180171

181-
JSON(w, http.StatusOK, GetEmbeddedClusterNodeJoinCommandResponse{
182-
ClusterID: install.Spec.ClusterID,
172+
clusterUUID, err := uuid.Parse(install.Spec.ClusterID)
173+
if err != nil {
174+
logger.Error(fmt.Errorf("failed to parse cluster id: %w", err))
175+
w.WriteHeader(http.StatusInternalServerError)
176+
return
177+
}
178+
179+
var currentAppVersionLabel string
180+
// attempt to get the current app version label from the installed app
181+
installedApps, err := store.GetStore().ListInstalledApps()
182+
if err != nil {
183+
logger.Error(fmt.Errorf("failed to list installed apps: %w", err))
184+
w.WriteHeader(http.StatusInternalServerError)
185+
return
186+
} else if len(installedApps) > 0 {
187+
// "CurrentSequence" is the latest available version of the app in a non-embedded cluster.
188+
// However, in an embedded cluster, the "CurrentSequence" is also the currently deployed version of the app.
189+
// This is because EC uses the new upgrade flow, which only creates a new app version when
190+
// the app version gets deployed. And because rollbacks are not supported in embedded cluster yet.
191+
appVersion, err := store.GetStore().GetAppVersion(installedApps[0].ID, installedApps[0].CurrentSequence)
192+
if err != nil {
193+
logger.Error(fmt.Errorf("failed to get app version: %w", err))
194+
w.WriteHeader(http.StatusInternalServerError)
195+
return
196+
}
197+
currentAppVersionLabel = appVersion.VersionLabel
198+
} else {
199+
// if there are no installed apps, we can't get the current app version label
200+
logger.Info("no installed apps found")
201+
}
202+
203+
JSON(w, http.StatusOK, join.JoinCommandResponse{
204+
ClusterID: clusterUUID,
183205
K0sJoinCommand: k0sJoinCommand,
184206
K0sToken: k0sToken,
185207
EmbeddedClusterVersion: ecVersion,
186208
AirgapRegistryAddress: airgapRegistryAddress,
187209
TCPConnectionsRequired: endpoints,
188210
InstallationSpec: install.Spec,
211+
AppVersionLabel: currentAppVersionLabel,
189212
})
190213
}

pkg/handlers/embedded_cluster_node_join_command_test.go

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import (
1111
"time"
1212

1313
gomock "github.com/golang/mock/gomock"
14+
"github.com/google/uuid"
1415
embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
16+
"github.com/replicatedhq/embedded-cluster/kinds/types/join"
17+
versionTypes "github.com/replicatedhq/kots/pkg/api/version/types"
18+
appTypes "github.com/replicatedhq/kots/pkg/app/types"
1519
"github.com/replicatedhq/kots/pkg/handlers/kubeclient"
1620
"github.com/replicatedhq/kots/pkg/store"
1721
mockstore "github.com/replicatedhq/kots/pkg/store/mock"
@@ -33,13 +37,15 @@ type testNodeJoinCommandHarness struct {
3337
token string
3438
getRoles func(t *testing.T, token string) ([]string, error)
3539
embeddedClusterID string
36-
validateBody func(t *testing.T, h *testNodeJoinCommandHarness, r *GetEmbeddedClusterNodeJoinCommandResponse)
40+
appVersionLabel string
41+
validateBody func(t *testing.T, h *testNodeJoinCommandHarness, r *join.JoinCommandResponse)
3742
}
3843

3944
func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
4045
scheme := runtime.NewScheme()
4146
corev1.AddToScheme(scheme)
4247
embeddedclusterv1beta1.AddToScheme(scheme)
48+
ecUUID := uuid.New().String()
4349

4450
tests := []testNodeJoinCommandHarness{
4551
{
@@ -50,15 +56,15 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
5056
{
5157
name: "store returns error",
5258
httpStatus: http.StatusInternalServerError,
53-
embeddedClusterID: "cluster-id",
59+
embeddedClusterID: ecUUID,
5460
getRoles: func(*testing.T, string) ([]string, error) {
5561
return nil, fmt.Errorf("some error")
5662
},
5763
},
5864
{
5965
name: "store gets passed the provided token",
6066
httpStatus: http.StatusInternalServerError,
61-
embeddedClusterID: "cluster-id",
67+
embeddedClusterID: ecUUID,
6268
token: "some-token",
6369
getRoles: func(t *testing.T, token string) ([]string, error) {
6470
require.Equal(t, "some-token", token)
@@ -68,8 +74,9 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
6874
{
6975
name: "bootstrap token secret creation succeeds and it matches returned K0SToken",
7076
httpStatus: http.StatusOK,
71-
embeddedClusterID: "cluster-id",
72-
validateBody: func(t *testing.T, h *testNodeJoinCommandHarness, r *GetEmbeddedClusterNodeJoinCommandResponse) {
77+
embeddedClusterID: ecUUID,
78+
appVersionLabel: "test-app-version",
79+
validateBody: func(t *testing.T, h *testNodeJoinCommandHarness, r *join.JoinCommandResponse) {
7380
req := require.New(t)
7481
// Check that a secret was created with the cluster bootstrap token
7582
var secrets corev1.SecretList
@@ -91,7 +98,11 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
9198
decompressed, err := util.GunzipData(decodedK0SToken)
9299
req.NoError(err)
93100

94-
require.Containsf(t, string(decompressed), fmt.Sprintf("token: %s", expectedToken), "expected K0sToken:\n%s\nto contain the generated bootstrap token: %s", string(decompressed), expectedToken)
101+
req.Containsf(string(decompressed), fmt.Sprintf("token: %s", expectedToken), "expected K0sToken:\n%s\nto contain the generated bootstrap token: %s", string(decompressed), expectedToken)
102+
103+
// returned embedded cluster and app version should match the expected values
104+
req.Equal(r.AppVersionLabel, "test-app-version")
105+
req.Equal(r.ClusterID.String(), ecUUID)
95106
},
96107
kbClient: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
97108
&embeddedclusterv1beta1.Installation{
@@ -100,6 +111,7 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
100111
},
101112
Spec: embeddedclusterv1beta1.InstallationSpec{
102113
BinaryName: "my-app",
114+
ClusterID: ecUUID,
103115
Config: &embeddedclusterv1beta1.ConfigSpec{
104116
Version: "v1.100.0",
105117
Roles: embeddedclusterv1beta1.Roles{
@@ -144,8 +156,9 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
144156
{
145157
name: "tcp connections required are returned based on the controller role provided",
146158
httpStatus: http.StatusOK,
147-
embeddedClusterID: "cluster-id",
148-
validateBody: func(t *testing.T, h *testNodeJoinCommandHarness, r *GetEmbeddedClusterNodeJoinCommandResponse) {
159+
embeddedClusterID: ecUUID,
160+
appVersionLabel: "test-app-version",
161+
validateBody: func(t *testing.T, h *testNodeJoinCommandHarness, r *join.JoinCommandResponse) {
149162
req := require.New(t)
150163

151164
req.Equal([]string{
@@ -166,6 +179,7 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
166179
},
167180
Spec: embeddedclusterv1beta1.InstallationSpec{
168181
BinaryName: "my-app",
182+
ClusterID: ecUUID,
169183
Config: &embeddedclusterv1beta1.ConfigSpec{
170184
Version: "v1.100.0",
171185
Roles: embeddedclusterv1beta1.Roles{
@@ -227,6 +241,65 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
227241
},
228242
).Build(),
229243
},
244+
{
245+
name: "If there is no installed app, the app version label should be empty",
246+
httpStatus: http.StatusOK,
247+
embeddedClusterID: ecUUID,
248+
validateBody: func(t *testing.T, h *testNodeJoinCommandHarness, r *join.JoinCommandResponse) {
249+
req := require.New(t)
250+
// returned embedded cluster and app version should match the expected values
251+
req.Equal(r.AppVersionLabel, "")
252+
req.Equal(r.ClusterID.String(), ecUUID)
253+
},
254+
kbClient: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
255+
&embeddedclusterv1beta1.Installation{
256+
ObjectMeta: metav1.ObjectMeta{
257+
Name: time.Now().Format("20060102150405"),
258+
},
259+
Spec: embeddedclusterv1beta1.InstallationSpec{
260+
BinaryName: "my-app",
261+
ClusterID: ecUUID,
262+
Config: &embeddedclusterv1beta1.ConfigSpec{
263+
Version: "v1.100.0",
264+
Roles: embeddedclusterv1beta1.Roles{
265+
Controller: embeddedclusterv1beta1.NodeRole{
266+
Name: "controller-role",
267+
},
268+
},
269+
},
270+
},
271+
},
272+
&corev1.ConfigMap{
273+
ObjectMeta: metav1.ObjectMeta{
274+
Name: "kube-root-ca.crt",
275+
Namespace: "kube-system",
276+
},
277+
Data: map[string]string{"ca.crt": "some-ca-cert"},
278+
},
279+
&corev1.Node{
280+
ObjectMeta: metav1.ObjectMeta{
281+
Name: "controller 1",
282+
Labels: map[string]string{
283+
"node-role.kubernetes.io/control-plane": "true",
284+
},
285+
},
286+
Status: corev1.NodeStatus{
287+
Conditions: []corev1.NodeCondition{
288+
{
289+
Type: corev1.NodeReady,
290+
Status: corev1.ConditionTrue,
291+
},
292+
},
293+
Addresses: []corev1.NodeAddress{
294+
{
295+
Type: corev1.NodeInternalIP,
296+
Address: "192.168.0.100",
297+
},
298+
},
299+
},
300+
},
301+
).Build(),
302+
},
230303
}
231304
for _, test := range tests {
232305
t.Run(test.name, func(t *testing.T) {
@@ -251,6 +324,24 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
251324
return []string{"controller-role", "worker-role"}, nil
252325
})
253326

327+
if test.appVersionLabel != "" {
328+
mockStore.EXPECT().ListInstalledApps().AnyTimes().DoAndReturn(func() ([]*appTypes.App, error) {
329+
return []*appTypes.App{
330+
{
331+
ID: "test-app-id",
332+
CurrentSequence: 1,
333+
},
334+
}, nil
335+
})
336+
mockStore.EXPECT().GetAppVersion("test-app-id", int64(1)).AnyTimes().DoAndReturn(func(appID string, sequence int64) (*versionTypes.AppVersion, error) {
337+
return &versionTypes.AppVersion{
338+
VersionLabel: test.appVersionLabel,
339+
}, nil
340+
})
341+
} else if test.httpStatus == http.StatusOK {
342+
mockStore.EXPECT().ListInstalledApps().AnyTimes().Return([]*appTypes.App{}, nil)
343+
}
344+
254345
// There's an early check in the handler for the presence of `EMBEDDED_CLUSTER_ID` env var
255346
// so we need to set it here whenever the test requires it
256347
if test.embeddedClusterID != "" {
@@ -274,7 +365,7 @@ func TestGetEmbeddedClusterNodeJoinCommand(t *testing.T) {
274365
}
275366

276367
// Run the body validation function if provided
277-
var body GetEmbeddedClusterNodeJoinCommandResponse
368+
var body join.JoinCommandResponse
278369
req.NoError(json.NewDecoder(response.Body).Decode(&body))
279370
if test.validateBody != nil {
280371
test.validateBody(t, &test, &body)

0 commit comments

Comments
 (0)