Skip to content

Commit 2139dea

Browse files
feat: e2e tests for multi node clusters (#285)
* feat: add script to getting join token adds a script to be used in our e2e tests. this script generates a join token for a controller join token. ```bash scripts: ./generate-controller-join-token.js 192.168.86.6 opening a new tab acessing kotsadm on port 30000 waiting and clickin on the 'Continue to Setup' button waiting and clicking on 'Advanced' to move on with the certificate waiting and clicking on 'Proceed' to move on with the certificate going to the /tls endpoint waiting and clicking on 'Continue' waiting and clicking in the password field typing the password clicking in the Log in button waiting and clicking in the Cluster Management tab waiting and clicking in the Add node button waiting and clicking in the controller role waiting and fetching the node join command sudo ./ec node join 192.168.86.6:30000 59hKbnRkZmAbFFbkZ8WoOGZX scripts: ``` * chore: adding script for worker and controller join tokens * chore: add e2e test for multi node clusters
1 parent 04f4f71 commit 2139dea

File tree

5 files changed

+493
-5
lines changed

5 files changed

+493
-5
lines changed

.github/workflows/e2e.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
- TestInstallWithDisabledAddons
1818
- TestHostPreflight
1919
- TestUnsupportedOverrides
20+
- TestMultiNodeInstallation
2021
steps:
2122
- name: Move Docker aside
2223
run: |

e2e/install_test.go

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@ package e2e
22

33
import (
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+
1021
func 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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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\nstderr: %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+
}

e2e/scripts/check-app-and-cluster-status.js

100755100644
File mode changed.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env node
2+
3+
/*
4+
* this script has been generated with chrome recorder and then pasted here.
5+
* some parts were manually changed, these are flagged with a CUSTOM comment.
6+
* all logging has also been manually added (process.stderr.write() calls).
7+
* this script is meant to be run as an argument to the `puppeteer.sh` script.
8+
* THIS SCRIPT EXPECTS THE STEP TO ENABLE HTTPS ACCESS TO KOTS TO BE ALREADY
9+
* COMPLETED. YOU NEED TO RUN check-app-and-cluster-status.js BEFORE THIS.
10+
*/
11+
12+
const puppeteer = require('puppeteer'); // v20.7.4 or later
13+
14+
(async () => {
15+
const browser = await puppeteer.launch(
16+
{
17+
headless: 'new',
18+
// CUSTOM: added the following line to fix the "No usable sandbox!" error.
19+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
20+
// CUSTOM: added ignore https errors.
21+
ignoreHTTPSErrors: true
22+
}
23+
);
24+
const page = await browser.newPage();
25+
const timeout = 5000;
26+
page.setDefaultTimeout(timeout);
27+
const args = process.argv.slice(2);
28+
if (args.length !== 1) {
29+
throw new Error('usage: generate-controller-join-token.js <kotsadm-ip>');
30+
}
31+
32+
{
33+
const targetPage = page;
34+
await targetPage.setViewport({
35+
width: 1512,
36+
height: 761
37+
})
38+
}
39+
{
40+
process.stderr.write("opening a new tab\n");
41+
const targetPage = page;
42+
const promises = [];
43+
const startWaitingForEvents = () => {
44+
promises.push(targetPage.waitForNavigation());
45+
}
46+
startWaitingForEvents();
47+
await targetPage.goto('chrome://new-tab-page/');
48+
await Promise.all(promises);
49+
}
50+
{
51+
process.stderr.write("acessing kotsadm on port 30000 (HTTPS)\n");
52+
const targetPage = page;
53+
const promises = [];
54+
const startWaitingForEvents = () => {
55+
promises.push(targetPage.waitForNavigation());
56+
}
57+
startWaitingForEvents();
58+
// CUSTOM: using the command line argument.
59+
await targetPage.goto(`https://${args[0]}:30000/`);
60+
await Promise.all(promises);
61+
}
62+
{
63+
process.stderr.write("waiting and clicking in the password field\n");
64+
const targetPage = page;
65+
await puppeteer.Locator.race([
66+
targetPage.locator('::-p-aria(password)'),
67+
targetPage.locator('input'),
68+
targetPage.locator('::-p-xpath(//*[@id=\\"app\\"]/div/div[2]/div/div/div/div[2]/div/div/div[1]/input)'),
69+
targetPage.locator(':scope >>> input')
70+
])
71+
.setTimeout(timeout)
72+
.click({
73+
offset: {
74+
x: 80,
75+
y: 21.0078125,
76+
},
77+
});
78+
}
79+
{
80+
process.stderr.write("typing the password\n");
81+
const targetPage = page;
82+
await puppeteer.Locator.race([
83+
targetPage.locator('::-p-aria(password)'),
84+
targetPage.locator('input'),
85+
targetPage.locator('::-p-xpath(//*[@id=\\"app\\"]/div/div[2]/div/div/div/div[2]/div/div/div[1]/input)'),
86+
targetPage.locator(':scope >>> input')
87+
])
88+
.setTimeout(timeout)
89+
.fill('password');
90+
}
91+
{
92+
process.stderr.write("clicking in the Log in button\n");
93+
const targetPage = page;
94+
await puppeteer.Locator.race([
95+
targetPage.locator('::-p-aria(Log in)'),
96+
targetPage.locator('button'),
97+
targetPage.locator('::-p-xpath(//*[@id=\\"app\\"]/div/div[2]/div/div/div/div[2]/div/div/div[2]/button)'),
98+
targetPage.locator(':scope >>> button')
99+
])
100+
.setTimeout(timeout)
101+
.click({
102+
offset: {
103+
x: 30,
104+
y: 14.0078125,
105+
},
106+
});
107+
}
108+
{
109+
process.stderr.write("waiting and clicking in the Cluster Management tab\n");
110+
const targetPage = page;
111+
await puppeteer.Locator.race([
112+
targetPage.locator('div:nth-of-type(3) > span'),
113+
targetPage.locator('::-p-xpath(//*[@id=\\"app\\"]/div/div[1]/div[1]/div[2]/div[3]/span)'),
114+
targetPage.locator(':scope >>> div:nth-of-type(3) > span'),
115+
targetPage.locator('::-p-text(Cluster Management)')
116+
])
117+
.setTimeout(timeout)
118+
.click({
119+
offset: {
120+
x: 108.734375,
121+
y: 28,
122+
},
123+
});
124+
}
125+
{
126+
process.stderr.write("waiting and clicking in the Add node button\n");
127+
const targetPage = page;
128+
await puppeteer.Locator.race([
129+
targetPage.locator('::-p-aria(Add node)'),
130+
targetPage.locator('div.tw-flex > button'),
131+
targetPage.locator('::-p-xpath(//*[@id=\\"app\\"]/div/div[2]/div/div/div[1]/button)'),
132+
targetPage.locator(':scope >>> div.tw-flex > button'),
133+
targetPage.locator('::-p-text(Add node)')
134+
])
135+
.setTimeout(timeout)
136+
.click({
137+
offset: {
138+
x: 16.328125,
139+
y: 13,
140+
},
141+
});
142+
}
143+
{
144+
process.stderr.write("waiting and clicking in the controller role\n");
145+
const targetPage = page;
146+
await puppeteer.Locator.race([
147+
targetPage.locator('div:nth-of-type(1) > label'),
148+
targetPage.locator('::-p-xpath(/html/body/div[5]/div/div/div/div[2]/div[1]/label)'),
149+
targetPage.locator(':scope >>> div:nth-of-type(1) > label')
150+
])
151+
.setTimeout(timeout)
152+
.click({
153+
offset: {
154+
x: 110,
155+
y: 27.5,
156+
},
157+
});
158+
}
159+
{
160+
// CUSTOM: finding the element that contains the node join command.
161+
process.stderr.write("waiting and fetching the node join command\n");
162+
const targetPage = page;
163+
await targetPage.waitForSelector('.react-prism.language-bash');
164+
let elementContent = await targetPage.evaluate(() => {
165+
const element = document.querySelector('.react-prism.language-bash');
166+
return element ? element.textContent : null;
167+
});
168+
if (!elementContent) {
169+
throw new Error("Could not find the node join command");
170+
}
171+
let result = { command: elementContent };
172+
console.log(JSON.stringify(result));
173+
}
174+
175+
await browser.close();
176+
177+
})().catch(err => {
178+
console.error(err);
179+
process.exit(1);
180+
});

0 commit comments

Comments
 (0)