From 3beab91a9e8e20b04e28e6297c6edb715ba8d46c Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Mon, 28 Apr 2025 11:12:13 -0700 Subject: [PATCH 1/7] feat(ec): update node join instructions for improved join experience --- pkg/embeddedcluster/node_join.go | 23 +++++++---- pkg/embeddedcluster/node_join_test.go | 19 +++------ .../embedded_cluster_node_join_command.go | 19 ++------- .../apps/EmbeddedClusterManagement.tsx | 40 +++++++++++++------ 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/pkg/embeddedcluster/node_join.go b/pkg/embeddedcluster/node_join.go index 90f4d057fc..2a75b33e4d 100644 --- a/pkg/embeddedcluster/node_join.go +++ b/pkg/embeddedcluster/node_join.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "net" "strings" "sync" "time" @@ -208,12 +209,12 @@ func findInternalIPAddress(addresses []corev1.NodeAddress) *corev1.NodeAddress { return nil } -// GenerateAddNodeCommand returns the command a user should run to add a node with the provided token -// the command will be of the form 'embeddedcluster node join ip:port UUID' -func GenerateAddNodeCommand(ctx context.Context, kbClient kbclient.Client, token string, isAirgap bool) (string, error) { +// GenerateAddNodeCommand returns a list of commands a user should run to add a node with the +// provided token. +func GenerateAddNodeCommand(ctx context.Context, kbClient kbclient.Client, token string) ([]string, error) { installation, err := GetCurrentInstallation(ctx, kbClient) if err != nil { - return "", fmt.Errorf("failed to get current installation: %w", err) + return nil, fmt.Errorf("failed to get current installation: %w", err) } binaryName := installation.Spec.BinaryName @@ -221,16 +222,24 @@ func GenerateAddNodeCommand(ctx context.Context, kbClient kbclient.Client, token // get the IP of a controller node nodeIP, err := getControllerNodeIP(ctx, kbClient) if err != nil { - return "", fmt.Errorf("failed to get controller node IP: %w", err) + return nil, fmt.Errorf("failed to get controller node IP: %w", err) } // get the port of the 'admin-console' service port, err := getAdminConsolePort(ctx, kbClient) if err != nil { - return "", fmt.Errorf("failed to get admin console port: %w", err) + return nil, fmt.Errorf("failed to get admin console port: %w", err) } - return fmt.Sprintf("sudo ./%s join %s:%d %s", binaryName, nodeIP, port, token), nil + address := net.JoinHostPort(nodeIP, fmt.Sprintf("%d", port)) + + commands := []string{ + fmt.Sprintf("curl -k https://%s/api/v1/embedded-cluster/binary -o %s.tar.gz", address, binaryName), + fmt.Sprintf("tar -xvf %s.tar.gz", binaryName), + fmt.Sprintf("sudo ./%s join %s %s", binaryName, address, token), + } + + return commands, nil } // GenerateK0sJoinCommand returns the k0s node join command, without the token but with all other required flags diff --git a/pkg/embeddedcluster/node_join_test.go b/pkg/embeddedcluster/node_join_test.go index 18c1ec7463..5519a5e391 100644 --- a/pkg/embeddedcluster/node_join_test.go +++ b/pkg/embeddedcluster/node_join_test.go @@ -80,25 +80,18 @@ func TestGenerateAddNodeCommand(t *testing.T) { req := require.New(t) - // Generate the add node command for online - gotCommand, err := GenerateAddNodeCommand(context.Background(), kbClient, "token", false) + gotCommand, err := GenerateAddNodeCommand(context.Background(), kbClient, "token") if err != nil { t.Fatalf("Failed to generate add node command: %v", err) } // Verify the generated command - wantCommand := "sudo ./my-app join 192.168.0.100:30000 token" - req.Equal(wantCommand, gotCommand) - - // Generate the add node command for airgap - gotCommand, err = GenerateAddNodeCommand(context.Background(), kbClient, "token", true) - if err != nil { - t.Fatalf("Failed to generate add node command: %v", err) + wantCommands := []string{ + "curl -k https://192.168.0.100:30000/api/v1/embedded-cluster/binary -o my-app.tar.gz", + "tar -xvf my-app.tar.gz", + "sudo ./my-app join 192.168.0.100:30000 token", } - - // Verify the generated command - wantCommand = "sudo ./my-app join 192.168.0.100:30000 token" - req.Equal(wantCommand, gotCommand) + req.Equal(wantCommands, gotCommand) } func TestGetAllNodeIPAddresses(t *testing.T) { diff --git a/pkg/handlers/embedded_cluster_node_join_command.go b/pkg/handlers/embedded_cluster_node_join_command.go index 9c2c2d3304..89ccc5e5d7 100644 --- a/pkg/handlers/embedded_cluster_node_join_command.go +++ b/pkg/handlers/embedded_cluster_node_join_command.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" "github.com/replicatedhq/embedded-cluster/kinds/types/join" - "github.com/replicatedhq/kots/pkg/api/handlers/types" + "github.com/replicatedhq/kots/pkg/api/handlers/types" "github.com/replicatedhq/kots/pkg/embeddedcluster" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsutil" @@ -38,19 +38,6 @@ func (h *Handler) GenerateEmbeddedClusterNodeJoinCommand(w http.ResponseWriter, return } - apps, err := store.GetStore().ListInstalledApps() - if err != nil { - logger.Error(fmt.Errorf("failed to list installed apps: %w", err)) - w.WriteHeader(http.StatusInternalServerError) - return - } - if len(apps) == 0 { - logger.Error(fmt.Errorf("no installed apps found")) - w.WriteHeader(http.StatusInternalServerError) - return - } - app := apps[0] - kbClient, err := h.GetKubeClient(r.Context()) if err != nil { logger.Error(fmt.Errorf("failed to get kubeclient: %w", err)) @@ -58,7 +45,7 @@ func (h *Handler) GenerateEmbeddedClusterNodeJoinCommand(w http.ResponseWriter, return } - nodeJoinCommand, err := embeddedcluster.GenerateAddNodeCommand(r.Context(), kbClient, token, app.IsAirgap) + nodeJoinCommands, err := embeddedcluster.GenerateAddNodeCommand(r.Context(), kbClient, token) if err != nil { logger.Error(fmt.Errorf("failed to generate add node command: %w", err)) w.WriteHeader(http.StatusInternalServerError) @@ -66,7 +53,7 @@ func (h *Handler) GenerateEmbeddedClusterNodeJoinCommand(w http.ResponseWriter, } JSON(w, http.StatusOK, types.GenerateEmbeddedClusterNodeJoinCommandResponse{ - Command: []string{nodeJoinCommand}, + Command: nodeJoinCommands, }) } diff --git a/web/src/components/apps/EmbeddedClusterManagement.tsx b/web/src/components/apps/EmbeddedClusterManagement.tsx index 0d89660d54..8ef626441e 100644 --- a/web/src/components/apps/EmbeddedClusterManagement.tsx +++ b/web/src/components/apps/EmbeddedClusterManagement.tsx @@ -178,7 +178,7 @@ const EmbeddedClusterManagement = ({ }); type AddNodeCommandResponse = { - command: string; + command: string[]; expiry: string; }; @@ -438,6 +438,12 @@ const EmbeddedClusterManagement = ({ ); }; + const addNodesCommandInstructions = [ + "Download the installation assets", + "Extract the installation assets", + "Join the node to the cluster", + ]; + const AddNodeCommands = () => { return ( <> @@ -506,18 +512,26 @@ const EmbeddedClusterManagement = ({

)} {!generateAddNodeCommandLoading && generateAddNodeCommand?.command && ( - <> - Copied! - } - > - {generateAddNodeCommand?.command} - - +
+ {generateAddNodeCommand.command.map((command, index) => ( +
+ {addNodesCommandInstructions[index] && ( +

+ {addNodesCommandInstructions[index]} +

+ )} + Copied! + } + > + {command} + +
+ ))} +
)} From 871cb98ebf5f05a2583cc89557e64a92a8b8b41d Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Mon, 28 Apr 2025 11:18:02 -0700 Subject: [PATCH 2/7] f --- web/src/components/apps/EmbeddedClusterManagement.tsx | 2 +- web/src/components/modals/AddANodeModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/apps/EmbeddedClusterManagement.tsx b/web/src/components/apps/EmbeddedClusterManagement.tsx index 8ef626441e..5c444c66cc 100644 --- a/web/src/components/apps/EmbeddedClusterManagement.tsx +++ b/web/src/components/apps/EmbeddedClusterManagement.tsx @@ -431,7 +431,7 @@ const EmbeddedClusterManagement = ({ {rolesData?.roles && rolesData.roles.length > 1 && "Select one or more roles to assign to the new node."}{" "} - Copy the join command and run it on the machine you'd like to join to + Copy the join commands and run them on the machine you'd like to join to the cluster.

diff --git a/web/src/components/modals/AddANodeModal.tsx b/web/src/components/modals/AddANodeModal.tsx index 81ca038008..075dfc6a67 100644 --- a/web/src/components/modals/AddANodeModal.tsx +++ b/web/src/components/modals/AddANodeModal.tsx @@ -31,7 +31,7 @@ const AddANodeModal = ({ {rolesData?.roles && rolesData.roles.length > 1 && "Select one or more roles to assign to the new node. "} - Copy the join command and run it on the machine you'd like to join to + Copy the join commands and run them on the machine you'd like to join to the cluster.

{children} From f81132e5efb921eb96b261490fc4d5b2c92e6755 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Mon, 28 Apr 2025 12:37:34 -0700 Subject: [PATCH 3/7] rename command to commands --- pkg/api/handlers/types/types.go | 2 +- pkg/handlers/embedded_cluster_node_join_command.go | 2 +- web/src/components/apps/EmbeddedClusterManagement.tsx | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/api/handlers/types/types.go b/pkg/api/handlers/types/types.go index e72d03da18..57149a6bbc 100644 --- a/pkg/api/handlers/types/types.go +++ b/pkg/api/handlers/types/types.go @@ -109,5 +109,5 @@ type GenerateEmbeddedClusterNodeJoinCommandRequest struct { } type GenerateEmbeddedClusterNodeJoinCommandResponse struct { - Command []string `json:"command"` + Commands []string `json:"commands"` } diff --git a/pkg/handlers/embedded_cluster_node_join_command.go b/pkg/handlers/embedded_cluster_node_join_command.go index 89ccc5e5d7..dd9a9f6cab 100644 --- a/pkg/handlers/embedded_cluster_node_join_command.go +++ b/pkg/handlers/embedded_cluster_node_join_command.go @@ -53,7 +53,7 @@ func (h *Handler) GenerateEmbeddedClusterNodeJoinCommand(w http.ResponseWriter, } JSON(w, http.StatusOK, types.GenerateEmbeddedClusterNodeJoinCommandResponse{ - Command: nodeJoinCommands, + Commands: nodeJoinCommands, }) } diff --git a/web/src/components/apps/EmbeddedClusterManagement.tsx b/web/src/components/apps/EmbeddedClusterManagement.tsx index 5c444c66cc..f1ed7c97c4 100644 --- a/web/src/components/apps/EmbeddedClusterManagement.tsx +++ b/web/src/components/apps/EmbeddedClusterManagement.tsx @@ -178,7 +178,7 @@ const EmbeddedClusterManagement = ({ }); type AddNodeCommandResponse = { - command: string[]; + commands: string[]; expiry: string; }; @@ -511,9 +511,9 @@ const EmbeddedClusterManagement = ({ {generateAddNodeCommandError?.message}

)} - {!generateAddNodeCommandLoading && generateAddNodeCommand?.command && ( + {!generateAddNodeCommandLoading && generateAddNodeCommand?.commands && (
- {generateAddNodeCommand.command.map((command, index) => ( + {generateAddNodeCommand.commands.map((command, index) => (
{addNodesCommandInstructions[index] && (

From f3882281852a2539937796b7382939e53c975812 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Mon, 28 Apr 2025 13:24:17 -0700 Subject: [PATCH 4/7] get join-command --- cmd/kots/cli/get-joincommand.go | 6 +++--- cmd/kots/cli/get-joincommand_test.go | 24 ++++++++++++++++-------- cmd/kots/cli/get.go | 10 ---------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/cmd/kots/cli/get-joincommand.go b/cmd/kots/cli/get-joincommand.go index 554739e5fe..b84f02a4e8 100644 --- a/cmd/kots/cli/get-joincommand.go +++ b/cmd/kots/cli/get-joincommand.go @@ -49,7 +49,7 @@ func GetJoinCmd() *cobra.Command { format := v.GetString("output") if format == "string" || format == "" { - fmt.Println(strings.Join(joinCmd, " ")) + fmt.Println(strings.Join(joinCmd, " && \n ")) return nil } else if format == "json" { type joinCommandResponse struct { @@ -58,7 +58,7 @@ func GetJoinCmd() *cobra.Command { joinCmdResponse := joinCommandResponse{ Command: joinCmd, } - b, err := json.Marshal(joinCmdResponse) + b, err := json.MarshalIndent(joinCmdResponse, "", " ") if err != nil { return fmt.Errorf("failed to marshal join command: %w", err) } @@ -117,7 +117,7 @@ func getJoinCommandCmd(ctx context.Context, clientset kubernetes.Interface, name return nil, fmt.Errorf("failed to get join command: %w", err) } - return joinCommand.Command, nil + return joinCommand.Commands, nil } // determine the embedded cluster roles list from /api/v1/embedded-cluster/roles diff --git a/cmd/kots/cli/get-joincommand_test.go b/cmd/kots/cli/get-joincommand_test.go index 579cf78dc1..53a92dc424 100644 --- a/cmd/kots/cli/get-joincommand_test.go +++ b/cmd/kots/cli/get-joincommand_test.go @@ -16,7 +16,7 @@ import ( "k8s.io/client-go/kubernetes/fake" ) -func TestGetJoinCommand(t *testing.T) { +func Test_getJoinCommandCmd(t *testing.T) { tests := []struct { name string service *corev1.Service @@ -59,7 +59,7 @@ func TestGetJoinCommand(t *testing.T) { "controllerRoleName": "test-controller-role-name", } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) case "POST": require.Equal(t, "/api/v1/embedded-cluster/generate-node-join-command", r.URL.Path) require.Equal(t, "test-auth-token", r.Header.Get("Authorization")) @@ -73,13 +73,21 @@ func TestGetJoinCommand(t *testing.T) { require.Equal(t, []string{"test-controller-role-name"}, requestBody.Roles) response := map[string][]string{ - "command": {"embedded-cluster", "join", "--token", "test-token"}, + "commands": { + "curl -k https://172.17.0.2:30000/api/v1/embedded-cluster/binary -o embedded-cluster.tar.gz", + "tar -xzf embedded-cluster.tar.gz", + "sudo ./embedded-cluster join 172.17.0.2:30000 7nPgRfWVZ3QIRWOnzsITEpLt", + }, } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) } }, - expectedCmd: []string{"embedded-cluster", "join", "--token", "test-token"}, + expectedCmd: []string{ + "curl -k https://172.17.0.2:30000/api/v1/embedded-cluster/binary -o embedded-cluster.tar.gz", + "tar -xzf embedded-cluster.tar.gz", + "sudo ./embedded-cluster join 172.17.0.2:30000 7nPgRfWVZ3QIRWOnzsITEpLt", + }, }, { name: "missing service", @@ -115,7 +123,7 @@ func TestGetJoinCommand(t *testing.T) { "error": "internal server error", } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) }, expectedError: "failed to get roles: unexpected status code: 500", }, @@ -154,14 +162,14 @@ func TestGetJoinCommand(t *testing.T) { "controllerRoleName": "test-controller-role-name", } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) case "POST": w.WriteHeader(http.StatusInternalServerError) response := map[string]string{ "error": "internal server error", } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) } }, expectedError: "failed to get join command: unexpected status code: 500", diff --git a/cmd/kots/cli/get.go b/cmd/kots/cli/get.go index 2d21831c87..5d9adc2426 100644 --- a/cmd/kots/cli/get.go +++ b/cmd/kots/cli/get.go @@ -1,8 +1,6 @@ package cli import ( - "os" - "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -19,14 +17,6 @@ kubectl kots get apps`, PreRun: func(cmd *cobra.Command, args []string) { viper.BindPFlags(cmd.Flags()) }, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - cmd.Help() - os.Exit(1) - } - - return nil - }, } cmd.AddCommand(GetAppsCmd()) From 5e18635e12a2b8d2b8e31fd68dd7264428f06dc1 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Mon, 28 Apr 2025 13:25:56 -0700 Subject: [PATCH 5/7] get join-command --- cmd/kots/cli/get-joincommand.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/kots/cli/get-joincommand.go b/cmd/kots/cli/get-joincommand.go index b84f02a4e8..0a731646d6 100644 --- a/cmd/kots/cli/get-joincommand.go +++ b/cmd/kots/cli/get-joincommand.go @@ -53,10 +53,10 @@ func GetJoinCmd() *cobra.Command { return nil } else if format == "json" { type joinCommandResponse struct { - Command []string `json:"command"` + Commands []string `json:"commands"` } joinCmdResponse := joinCommandResponse{ - Command: joinCmd, + Commands: joinCmd, } b, err := json.MarshalIndent(joinCmdResponse, "", " ") if err != nil { From 08c014c62d403a84d9eee7423eb041d56d4369f0 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Mon, 28 Apr 2025 13:39:17 -0700 Subject: [PATCH 6/7] step numbers --- web/src/components/apps/EmbeddedClusterManagement.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/components/apps/EmbeddedClusterManagement.tsx b/web/src/components/apps/EmbeddedClusterManagement.tsx index f1ed7c97c4..b609878095 100644 --- a/web/src/components/apps/EmbeddedClusterManagement.tsx +++ b/web/src/components/apps/EmbeddedClusterManagement.tsx @@ -516,7 +516,8 @@ const EmbeddedClusterManagement = ({ {generateAddNodeCommand.commands.map((command, index) => (

{addNodesCommandInstructions[index] && ( -

+

+ {index + 1} {addNodesCommandInstructions[index]}

)} From bb49634f71d7cf62101cc2d0217b72c55c910ed9 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Mon, 28 Apr 2025 14:15:25 -0700 Subject: [PATCH 7/7] feedback --- cmd/kots/cli/get-joincommand.go | 2 +- web/src/components/apps/EmbeddedClusterManagement.tsx | 6 +++--- web/src/components/modals/AddANodeModal.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/kots/cli/get-joincommand.go b/cmd/kots/cli/get-joincommand.go index 0a731646d6..9af630e7e2 100644 --- a/cmd/kots/cli/get-joincommand.go +++ b/cmd/kots/cli/get-joincommand.go @@ -49,7 +49,7 @@ func GetJoinCmd() *cobra.Command { format := v.GetString("output") if format == "string" || format == "" { - fmt.Println(strings.Join(joinCmd, " && \n ")) + fmt.Println(strings.Join(joinCmd, " && \\\n ")) return nil } else if format == "json" { type joinCommandResponse struct { diff --git a/web/src/components/apps/EmbeddedClusterManagement.tsx b/web/src/components/apps/EmbeddedClusterManagement.tsx index b609878095..39c34b978a 100644 --- a/web/src/components/apps/EmbeddedClusterManagement.tsx +++ b/web/src/components/apps/EmbeddedClusterManagement.tsx @@ -431,7 +431,7 @@ const EmbeddedClusterManagement = ({ {rolesData?.roles && rolesData.roles.length > 1 && "Select one or more roles to assign to the new node."}{" "} - Copy the join commands and run them on the machine you'd like to join to + Copy the commands and run them on the machine you'd like to join to the cluster.

@@ -439,8 +439,8 @@ const EmbeddedClusterManagement = ({ }; const addNodesCommandInstructions = [ - "Download the installation assets", - "Extract the installation assets", + "Download the binary on the new node", + "Extract the binary", "Join the node to the cluster", ]; diff --git a/web/src/components/modals/AddANodeModal.tsx b/web/src/components/modals/AddANodeModal.tsx index 075dfc6a67..883fa23ca8 100644 --- a/web/src/components/modals/AddANodeModal.tsx +++ b/web/src/components/modals/AddANodeModal.tsx @@ -31,7 +31,7 @@ const AddANodeModal = ({ {rolesData?.roles && rolesData.roles.length > 1 && "Select one or more roles to assign to the new node. "} - Copy the join commands and run them on the machine you'd like to join to + Copy the commands and run them on the machine you'd like to join to the cluster.

{children}