Skip to content

feat(ec): update node join instructions for improved join experience #5304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cmd/kots/cli/get-joincommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ 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 {
Command []string `json:"command"`
Commands []string `json:"commands"`
}
joinCmdResponse := joinCommandResponse{
Command: joinCmd,
Commands: joinCmd,
}
b, err := json.Marshal(joinCmdResponse)
b, err := json.MarshalIndent(joinCmdResponse, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal join command: %w", err)
}
Expand Down Expand Up @@ -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
Expand Down
24 changes: 16 additions & 8 deletions cmd/kots/cli/get-joincommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))
Expand All @@ -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",
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 0 additions & 10 deletions cmd/kots/cli/get.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cli

import (
"os"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/handlers/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,5 @@ type GenerateEmbeddedClusterNodeJoinCommandRequest struct {
}

type GenerateEmbeddedClusterNodeJoinCommandResponse struct {
Command []string `json:"command"`
Commands []string `json:"commands"`
}
23 changes: 16 additions & 7 deletions pkg/embeddedcluster/node_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"net"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -208,29 +209,37 @@ 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

// 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
Expand Down
19 changes: 6 additions & 13 deletions pkg/embeddedcluster/node_join_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 3 additions & 16 deletions pkg/handlers/embedded_cluster_node_join_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -38,35 +38,22 @@ 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))
w.WriteHeader(http.StatusInternalServerError)
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)
return
}

JSON(w, http.StatusOK, types.GenerateEmbeddedClusterNodeJoinCommandResponse{
Command: []string{nodeJoinCommand},
Commands: nodeJoinCommands,
})
}

Expand Down
45 changes: 30 additions & 15 deletions web/src/components/apps/EmbeddedClusterManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ const EmbeddedClusterManagement = ({
});

type AddNodeCommandResponse = {
command: string;
commands: string[];
expiry: string;
};

Expand Down Expand Up @@ -431,13 +431,19 @@ 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 commands and run them on the machine you'd like to join to
the cluster.
</p>
</div>
);
};

const addNodesCommandInstructions = [
"Download the binary on the new node",
"Extract the binary",
"Join the node to the cluster",
];

const AddNodeCommands = () => {
return (
<>
Expand Down Expand Up @@ -505,19 +511,28 @@ const EmbeddedClusterManagement = ({
{generateAddNodeCommandError?.message}
</p>
)}
{!generateAddNodeCommandLoading && generateAddNodeCommand?.command && (
<>
<CodeSnippet
key={selectedNodeTypes.toString()}
language="bash"
canCopy={true}
onCopyText={
<span className="u-textColor--success">Copied!</span>
}
>
{generateAddNodeCommand?.command}
</CodeSnippet>
</>
{!generateAddNodeCommandLoading && generateAddNodeCommand?.commands && (
<div className="tw-flex tw-flex-col tw-gap-4 tw-mt-4">
{generateAddNodeCommand.commands.map((command, index) => (
<div key={command}>
{addNodesCommandInstructions[index] && (
<p className="tw-text-gray-600 tw-font-semibold tw-flex tw-items-center tw-gap-2">
<span className="tw-inline-block tw-rounded-full tw-text-white tw-bg-[#326DE6] tw-text-sm tw-w-5 tw-h-5 tw-flex tw-items-center tw-justify-center">{index + 1}</span>
{addNodesCommandInstructions[index]}
</p>
)}
<CodeSnippet
language="bash"
canCopy={true}
onCopyText={
<span className="u-textColor--success">Copied!</span>
}
>
{command}
</CodeSnippet>
</div>
))}
</div>
)}
</div>
</>
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/modals/AddANodeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 commands and run them on the machine you'd like to join to
the cluster.
</p>
{children}
Expand Down
Loading