Skip to content

Commit 3122016

Browse files
authored
add hidden 'get join-command' command for embedded-cluster (#5280)
* add hidden 'get join-command' command for embedded-cluster * update unit test code check error codes, test error code handling in both requests
1 parent 3a0c493 commit 3122016

File tree

7 files changed

+431
-19
lines changed

7 files changed

+431
-19
lines changed

cmd/kots/cli/get-joincommand.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package cli
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"strings"
11+
12+
"github.com/replicatedhq/kots/pkg/api/handlers/types"
13+
"github.com/replicatedhq/kots/pkg/auth"
14+
"github.com/replicatedhq/kots/pkg/k8sutil"
15+
"github.com/spf13/cobra"
16+
"github.com/spf13/viper"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
"k8s.io/client-go/kubernetes"
19+
)
20+
21+
func GetJoinCmd() *cobra.Command {
22+
cmd := &cobra.Command{
23+
Use: "join-command",
24+
Short: "Get embedded cluster join command",
25+
Long: "",
26+
SilenceUsage: false,
27+
SilenceErrors: false,
28+
Hidden: true,
29+
PreRun: func(cmd *cobra.Command, args []string) {
30+
viper.BindPFlags(cmd.Flags())
31+
},
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
v := viper.GetViper()
34+
35+
clientset, err := k8sutil.GetClientset()
36+
if err != nil {
37+
return fmt.Errorf("failed to get clientset: %w", err)
38+
}
39+
40+
namespace, err := getNamespaceOrDefault(v.GetString("namespace"))
41+
if err != nil {
42+
return fmt.Errorf("failed to get namespace: %w", err)
43+
}
44+
45+
joinCmd, err := getJoinCommandCmd(cmd.Context(), clientset, namespace)
46+
if err != nil {
47+
return err
48+
}
49+
50+
format := v.GetString("output")
51+
if format == "string" || format == "" {
52+
fmt.Println(strings.Join(joinCmd, " "))
53+
return nil
54+
} else if format == "json" {
55+
type joinCommandResponse struct {
56+
Command []string `json:"command"`
57+
}
58+
joinCmdResponse := joinCommandResponse{
59+
Command: joinCmd,
60+
}
61+
b, err := json.Marshal(joinCmdResponse)
62+
if err != nil {
63+
return fmt.Errorf("failed to marshal join command: %w", err)
64+
}
65+
fmt.Println(string(b))
66+
return nil
67+
}
68+
69+
return fmt.Errorf("invalid output format: %s", format)
70+
},
71+
}
72+
cmd.Flags().StringP("output", "o", "", "output format (currently supported: json)")
73+
74+
return cmd
75+
}
76+
77+
func getJoinCommandCmd(ctx context.Context, clientset kubernetes.Interface, namespace string) ([]string, error) {
78+
// determine the IP address and port of the kotsadm service
79+
// this only runs inside an embedded cluster and so we don't need to setup port forwarding
80+
svc, err := clientset.CoreV1().Services(namespace).Get(ctx, "kotsadm", metav1.GetOptions{})
81+
if err != nil {
82+
return nil, fmt.Errorf("unable to get kotsadm service: %w", err)
83+
}
84+
kotsadmIP := svc.Spec.ClusterIP
85+
if kotsadmIP == "" {
86+
return nil, fmt.Errorf("kotsadm service ip was empty")
87+
}
88+
89+
if len(svc.Spec.Ports) == 0 {
90+
return nil, fmt.Errorf("kotsadm service ports were empty")
91+
}
92+
kotsadmPort := svc.Spec.Ports[0].Port
93+
94+
authSlug, err := auth.GetOrCreateAuthSlug(clientset, namespace)
95+
if err != nil {
96+
return nil, fmt.Errorf("failed to get kotsadm auth slug: %w", err)
97+
}
98+
99+
url := fmt.Sprintf("http://%s:%d/api/v1/embedded-cluster/roles", kotsadmIP, kotsadmPort)
100+
roles, err := getRoles(url, authSlug)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to get roles: %w", err)
103+
}
104+
105+
controllerRole := roles.ControllerRoleName
106+
if controllerRole == "" && len(roles.Roles) > 0 {
107+
controllerRole = roles.Roles[0]
108+
}
109+
if controllerRole == "" {
110+
return nil, fmt.Errorf("unable to determine controller role name")
111+
}
112+
113+
// get a join command with the controller role with a post to /api/v1/embedded-cluster/generate-node-join-command
114+
url = fmt.Sprintf("http://%s:%d/api/v1/embedded-cluster/generate-node-join-command", kotsadmIP, kotsadmPort)
115+
joinCommand, err := getJoinCommand(url, authSlug, []string{controllerRole})
116+
if err != nil {
117+
return nil, fmt.Errorf("failed to get join command: %w", err)
118+
}
119+
120+
return joinCommand.Command, nil
121+
}
122+
123+
// determine the embedded cluster roles list from /api/v1/embedded-cluster/roles
124+
func getRoles(url string, authSlug string) (*types.GetEmbeddedClusterRolesResponse, error) {
125+
newReq, err := http.NewRequest("GET", url, nil)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to create request: %w", err)
128+
}
129+
newReq.Header.Add("Content-Type", "application/json")
130+
newReq.Header.Add("Authorization", authSlug)
131+
132+
resp, err := http.DefaultClient.Do(newReq)
133+
if err != nil {
134+
return nil, fmt.Errorf("failed to execute request: %w", err)
135+
}
136+
defer resp.Body.Close()
137+
138+
if resp.StatusCode != http.StatusOK {
139+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
140+
}
141+
142+
b, err := io.ReadAll(resp.Body)
143+
if err != nil {
144+
return nil, fmt.Errorf("failed to read response body: %w", err)
145+
}
146+
147+
roles := &types.GetEmbeddedClusterRolesResponse{}
148+
if err := json.Unmarshal(b, roles); err != nil {
149+
return nil, fmt.Errorf("failed to unmarshal roles: %w", err)
150+
}
151+
152+
return roles, nil
153+
}
154+
155+
func getJoinCommand(url string, authSlug string, roles []string) (*types.GenerateEmbeddedClusterNodeJoinCommandResponse, error) {
156+
payload := types.GenerateEmbeddedClusterNodeJoinCommandRequest{
157+
Roles: roles,
158+
}
159+
b, err := json.Marshal(payload)
160+
if err != nil {
161+
return nil, fmt.Errorf("failed to marshal roles: %w", err)
162+
}
163+
164+
newReq, err := http.NewRequest("POST", url, bytes.NewBuffer(b))
165+
if err != nil {
166+
return nil, fmt.Errorf("failed to create request: %w", err)
167+
}
168+
newReq.Header.Add("Content-Type", "application/json")
169+
newReq.Header.Add("Authorization", authSlug)
170+
171+
resp, err := http.DefaultClient.Do(newReq)
172+
if err != nil {
173+
return nil, fmt.Errorf("failed to execute request: %w", err)
174+
}
175+
defer resp.Body.Close()
176+
177+
if resp.StatusCode != http.StatusOK {
178+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
179+
}
180+
181+
fullResponse, err := io.ReadAll(resp.Body)
182+
if err != nil {
183+
return nil, fmt.Errorf("failed to read response body: %w", err)
184+
}
185+
186+
joinCommand := &types.GenerateEmbeddedClusterNodeJoinCommandResponse{}
187+
if err := json.Unmarshal(fullResponse, joinCommand); err != nil {
188+
return nil, fmt.Errorf("failed to unmarshal roles: %w", err)
189+
}
190+
191+
return joinCommand, nil
192+
}

0 commit comments

Comments
 (0)