@@ -2,11 +2,22 @@ package e2e
22
33import (
44 "encoding/json"
5+ "strings"
56 "testing"
7+ "time"
68
79 "github.com/replicatedhq/embedded-cluster/e2e/cluster"
810)
911
12+ type clusterStatusResponse struct {
13+ App string `json:"app"`
14+ Cluster string `json:"cluster"`
15+ }
16+
17+ type nodeJoinResponse struct {
18+ Command string `json:"command"`
19+ }
20+
1021func TestSingleNodeInstallation (t * testing.T ) {
1122 t .Parallel ()
1223 tc := cluster .NewTestCluster (& cluster.Input {
@@ -48,11 +59,7 @@ func TestSingleNodeInstallation(t *testing.T) {
4859 t .Log ("stderr:" , stderr )
4960 t .Fatalf ("fail to access kotsadm interface and state: %v" , err )
5061 }
51- type response struct {
52- App string `json:"app"`
53- Cluster string `json:"cluster"`
54- }
55- var r response
62+ var r clusterStatusResponse
5663 if err := json .Unmarshal ([]byte (stdout ), & r ); err != nil {
5764 t .Log ("stdout:" , stdout )
5865 t .Log ("stderr:" , stderr )
@@ -246,3 +253,121 @@ func TestHostPreflight(t *testing.T) {
246253 t .Fatalf ("fail to install embedded-cluster on node %s: %v" , tc .Nodes [0 ], err )
247254 }
248255}
256+
257+ // This test creates 4 nodes, installs on the first one and then generate 2 join tokens
258+ // for controllers and one join token for worker nodes. Joins the nodes and then waits
259+ // for them to report ready.
260+ func TestMultiNodeInstallation (t * testing.T ) {
261+ tc := cluster .NewTestCluster (& cluster.Input {
262+ T : t ,
263+ Nodes : 4 ,
264+ Image : "ubuntu/jammy" ,
265+ SSHPublicKey : "../output/tmp/id_rsa.pub" ,
266+ SSHPrivateKey : "../output/tmp/id_rsa" ,
267+ EmbeddedClusterPath : "../output/bin/embedded-cluster" ,
268+ })
269+ defer tc .Destroy ()
270+ t .Log ("installing ssh on node 0" )
271+ commands := [][]string {{"apt-get" , "update" , "-y" }, {"apt-get" , "install" , "openssh-server" , "-y" }}
272+ if err := RunCommandsOnNode (t , tc , 0 , commands ); err != nil {
273+ t .Fatalf ("fail to install ssh on node %s: %v" , tc .Nodes [0 ], err )
274+ }
275+
276+ // bootstrap the first node and makes sure it is healthy. also executes the kots
277+ // ssl certificate configuration (kurl-proxy).
278+ t .Log ("installing embedded-cluster on node 0" )
279+ if stdout , stderr , err := RunCommandOnNode (t , tc , 0 , []string {"single-node-install.sh" }); err != nil {
280+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
281+ t .Fatalf ("fail to install embedded-cluster on node %s: %v" , tc .Nodes [0 ], err )
282+ }
283+ t .Log ("installing puppeteer on node 0" )
284+ if stdout , stderr , err := RunCommandOnNode (t , tc , 0 , []string {"install-puppeteer.sh" }); err != nil {
285+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
286+ t .Fatalf ("fail to install puppeteer on node %s: %v" , tc .Nodes [0 ], err )
287+ }
288+ t .Log ("accessing kotsadm interface and checking app and cluster state" )
289+ line := []string {"puppeteer.sh" , "check-app-and-cluster-status.js" , "10.0.0.2" }
290+ stdout , stderr , err := RunCommandOnNode (t , tc , 0 , line )
291+ if err != nil {
292+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
293+ t .Fatalf ("fail to access kotsadm interface and state: %v" , err )
294+ }
295+ var r clusterStatusResponse
296+ if err := json .Unmarshal ([]byte (stdout ), & r ); err != nil {
297+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
298+ t .Fatalf ("fail to parse script response: %v" , err )
299+ } else if r .App != "Ready" || r .Cluster != "Up to date" {
300+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
301+ t .Fatalf ("cluster or app not ready: %s" , stdout )
302+ }
303+
304+ // generate all node join commands (2 for controllers and 1 for worker).
305+ t .Log ("generating two new controller token commands" )
306+ controllerCommands := []string {}
307+ for i := 0 ; i < 2 ; i ++ {
308+ line = []string {"puppeteer.sh" , "generate-controller-join-token.js" , "10.0.0.2" }
309+ stdout , stderr , err := RunCommandOnNode (t , tc , 0 , line )
310+ if err != nil {
311+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
312+ t .Fatalf ("fail to generate controller join token: %s" , stdout )
313+ }
314+ var r nodeJoinResponse
315+ if err := json .Unmarshal ([]byte (stdout ), & r ); err != nil {
316+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
317+ t .Fatalf ("fail to parse script response: %v" , err )
318+ }
319+ // trim down the "./" and the "sudo" command as those are not needed. we run as
320+ // root and the embedded-cluster binary is on the PATH.
321+ command := strings .TrimPrefix (r .Command , "sudo ./" )
322+ controllerCommands = append (controllerCommands , command )
323+ t .Log ("controller join token command:" , command )
324+ }
325+ t .Log ("generating a new worker token command" )
326+ line = []string {"puppeteer.sh" , "generate-worker-join-token.js" , "10.0.0.2" }
327+ stdout , stderr , err = RunCommandOnNode (t , tc , 0 , line )
328+ if err != nil {
329+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
330+ t .Fatalf ("fail to generate controller join token: %s" , stdout )
331+ }
332+ var jr nodeJoinResponse
333+ if err := json .Unmarshal ([]byte (stdout ), & jr ); err != nil {
334+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
335+ t .Fatalf ("fail to parse script response: %v" , err )
336+ }
337+
338+ // join the nodes.
339+ for i , cmd := range controllerCommands {
340+ node := i + 1
341+ t .Logf ("joining node %d to the cluster (controller)" , node )
342+ stdout , stderr , err := RunCommandOnNode (t , tc , node , strings .Split (cmd , " " ))
343+ if err != nil {
344+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
345+ t .Fatalf ("fail to join node %d as a controller: %v" , node , err )
346+ }
347+ // XXX If we are too aggressive joining nodes we can see the following error being
348+ // thrown by kotsadm on its log (and we get a 500 back):
349+ // "
350+ // failed to get controller role name: failed to get cluster config: failed to get
351+ // current installation: failed to list installations: etcdserver: leader changed
352+ // "
353+ t .Logf ("node %d joined, sleeping..." , node )
354+ time .Sleep (30 * time .Second )
355+ }
356+ command := strings .TrimPrefix (jr .Command , "sudo ./" )
357+ t .Log ("worker join token command:" , command )
358+ t .Log ("joining node 3 to the cluster as a worker" )
359+ stdout , stderr , err = RunCommandOnNode (t , tc , 3 , strings .Split (command , " " ))
360+ if err != nil {
361+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
362+ t .Fatalf ("fail to join node 3 to the cluster as a worker: %v" , err )
363+ }
364+
365+ // wait for the nodes to report as ready.
366+ t .Log ("all nodes joined, waiting for them to be ready" )
367+ stdout , stderr , err = RunCommandOnNode (t , tc , 0 , []string {"wait-for-ready-nodes.sh" , "4" })
368+ if err != nil {
369+ t .Logf ("stdout: %s\n stderr: %s" , stdout , stderr )
370+ t .Fatalf ("fail to install embedded-cluster on node %s: %v" , tc .Nodes [0 ], err )
371+ }
372+ t .Log (stdout )
373+ }
0 commit comments