Skip to content

Commit 0c0ba24

Browse files
committed
chore(ci): run e2e tests on cmx
1 parent ee6aad4 commit 0c0ba24

File tree

6 files changed

+541
-21
lines changed

6 files changed

+541
-21
lines changed

e2e/cluster/cmx/cluster.go

+342
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
package cmx
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strconv"
11+
"strings"
12+
"testing"
13+
14+
"github.com/google/uuid"
15+
)
16+
17+
const (
18+
DefaultDistribution = "ubuntu"
19+
DefaultVersion = "22.04"
20+
DefaultTTL = "2h"
21+
DefaultInstanceType = "r1.medium"
22+
DefaultDiskSize = 50
23+
)
24+
25+
// Cluster implements the cluster.Cluster interface for Replicated VM
26+
type Cluster struct {
27+
t *testing.T
28+
29+
gid string
30+
networkID string
31+
nodes []*node
32+
proxyNode *node
33+
sshUser string
34+
}
35+
36+
type ClusterInput struct {
37+
T *testing.T
38+
Nodes int
39+
Distribution string
40+
Version string
41+
WithProxy bool
42+
LicensePath string
43+
EmbeddedClusterPath string
44+
AirgapInstallBundlePath string
45+
AirgapUpgradeBundlePath string
46+
}
47+
48+
// NewCluster creates a new CMX cluster using the provided configuration
49+
func NewCluster(ctx context.Context, input ClusterInput) *Cluster {
50+
if input.T == nil {
51+
panic("testing.T is required")
52+
}
53+
54+
sshUser := os.Getenv("REPLICATEDVM_SSH_USER")
55+
if sshUser == "" {
56+
input.T.Fatalf("REPLICATEDVM_SSH_USER is not set")
57+
}
58+
59+
c := &Cluster{
60+
t: input.T,
61+
sshUser: sshUser,
62+
gid: uuid.New().String(),
63+
}
64+
c.t.Cleanup(c.destroy)
65+
66+
c.t.Logf("Creating network")
67+
network, err := createNetwork(ctx, c.gid, DefaultTTL)
68+
if err != nil {
69+
c.t.Fatalf("Failed to create network: %v", err)
70+
}
71+
c.networkID = network.ID
72+
73+
c.t.Logf("Creating %d nodes", input.Nodes)
74+
nodes, err := createNodes(ctx, c.gid, c.networkID, DefaultTTL, clusterInputToCreateNodeOpts(input))
75+
if err != nil {
76+
c.t.Fatalf("Failed to create nodes: %v", err)
77+
}
78+
c.nodes = nodes
79+
80+
// If proxy is requested, create an additional node
81+
if input.WithProxy {
82+
c.t.Logf("Creating proxy node")
83+
proxyNodes, err := createNodes(ctx, c.gid, c.networkID, DefaultTTL, createNodeOpts{
84+
Distribution: DefaultDistribution,
85+
Version: DefaultVersion,
86+
Count: 1,
87+
InstanceType: "r1.small",
88+
DiskSize: 10,
89+
})
90+
if err != nil {
91+
c.t.Fatalf("Failed to create proxy node: %v", err)
92+
}
93+
c.proxyNode = proxyNodes[0]
94+
}
95+
96+
for _, node := range c.nodes {
97+
c.t.Logf("Copying files to node %s", node.ID)
98+
err := c.copyFilesToNode(ctx, node, input)
99+
if err != nil {
100+
c.t.Fatalf("Failed to copy files to node %s: %v", node.ID, err)
101+
}
102+
c.t.Logf("Copying dirs to node %s", node.ID)
103+
err = c.copyDirsToNode(ctx, node)
104+
if err != nil {
105+
c.t.Fatalf("Failed to copy dirs to node %s: %v", node.ID, err)
106+
}
107+
}
108+
109+
return c
110+
}
111+
112+
func clusterInputToCreateNodeOpts(input ClusterInput) createNodeOpts {
113+
opts := createNodeOpts{
114+
Distribution: input.Distribution,
115+
Version: input.Version,
116+
Count: input.Nodes,
117+
}
118+
if opts.Distribution == "" {
119+
opts.Distribution = DefaultDistribution
120+
}
121+
if opts.Version == "" {
122+
opts.Version = DefaultVersion
123+
}
124+
if opts.Count <= 0 {
125+
opts.Count = 1
126+
}
127+
if opts.InstanceType == "" {
128+
opts.InstanceType = DefaultInstanceType
129+
}
130+
if opts.DiskSize <= 0 {
131+
opts.DiskSize = DefaultDiskSize
132+
}
133+
return opts
134+
}
135+
136+
// Cleanup removes the VM instance
137+
func (c *Cluster) Cleanup(envs ...map[string]string) {
138+
// TODO: generate support bundle and copy playwright report
139+
c.destroy()
140+
}
141+
142+
func (c *Cluster) destroy() {
143+
if c.gid != "" {
144+
// Best effort cleanup
145+
c.t.Logf("Cleaning up nodes")
146+
err := deleteNodesByGroupID(context.Background(), c.gid)
147+
if err != nil {
148+
c.t.Logf("Failed to cleanup cluster: %v", err)
149+
}
150+
}
151+
152+
if c.networkID != "" {
153+
c.t.Logf("Cleaning up network %s", c.networkID)
154+
err := deleteNetwork(context.Background(), c.networkID)
155+
if err != nil {
156+
c.t.Logf("Failed to cleanup network: %v", err)
157+
}
158+
}
159+
}
160+
161+
// RunCommandOnNode executes a command on the specified node using replicated vm ssh
162+
func (c *Cluster) RunCommandOnNode(ctx context.Context, node int, command []string, envs ...map[string]string) (string, string, error) {
163+
c.t.Logf("Running command on node %s: %s", c.nodes[node].ID, strings.Join(command, " "))
164+
return c.runCommandOnNode(ctx, c.nodes[node], command, envs...)
165+
}
166+
167+
// RunCommandOnProxyNode executes a command on the proxy node
168+
func (c *Cluster) RunCommandOnProxyNode(ctx context.Context, command []string, envs ...map[string]string) (string, string, error) {
169+
c.t.Logf("Running command on proxy node: %s", strings.Join(command, " "))
170+
return c.runCommandOnNode(ctx, c.proxyNode, command, envs...)
171+
}
172+
173+
func (c *Cluster) Node(node int) *node {
174+
return c.nodes[node]
175+
}
176+
177+
func (c *Cluster) runCommandOnNode(ctx context.Context, node *node, command []string, envs ...map[string]string) (string, string, error) {
178+
args := []string{}
179+
args = append(args, sshConnectionArgs(node)...)
180+
args = append(args, "sh", "-c", strings.Join(command, " "))
181+
cmd := exec.CommandContext(ctx, "ssh", args...)
182+
183+
env := os.Environ()
184+
for _, e := range envs {
185+
for k, v := range e {
186+
env = append(env, fmt.Sprintf("%s=%s", k, v))
187+
}
188+
}
189+
cmd.Env = env
190+
191+
var outBuf, errBuf bytes.Buffer
192+
cmd.Stdout = &outBuf
193+
cmd.Stderr = &errBuf
194+
195+
err := cmd.Run()
196+
197+
stdout := outBuf.String()
198+
stderr := errBuf.String()
199+
200+
return stdout, stderr, err
201+
}
202+
203+
func (c *Cluster) copyFilesToNode(ctx context.Context, node *node, in ClusterInput) error {
204+
files := map[string]string{
205+
in.LicensePath: "/assets/license.yaml", //0644
206+
in.EmbeddedClusterPath: "/usr/local/bin/embedded-cluster", //0755
207+
in.AirgapInstallBundlePath: "/assets/ec-release.tgz", //0755
208+
in.AirgapUpgradeBundlePath: "/assets/ec-release-upgrade.tgz", //0755
209+
}
210+
for src, dest := range files {
211+
if src != "" {
212+
err := c.CopyFileToNode(ctx, node, src, dest)
213+
if err != nil {
214+
return fmt.Errorf("copy file %s to node %s at %s: %v", src, node.ID, dest, err)
215+
}
216+
}
217+
}
218+
return nil
219+
}
220+
221+
func (c *Cluster) copyDirsToNode(ctx context.Context, node *node) error {
222+
dirs := map[string]string{
223+
"../../../scripts": "/usr/local/bin",
224+
"playwright": "/automation/playwright",
225+
"../operator/charts/embedded-cluster-operator/troubleshoot": "/automation/troubleshoot",
226+
}
227+
for src, dest := range dirs {
228+
err := c.CopyDirToNode(ctx, node, src, dest)
229+
if err != nil {
230+
return fmt.Errorf("copy dir %s to node %s at %s: %v", src, node.ID, dest, err)
231+
}
232+
}
233+
return nil
234+
}
235+
236+
func (c *Cluster) CopyFileToNode(ctx context.Context, node *node, src, dest string) error {
237+
c.t.Logf("Copying file %s to node %s at %s", src, node.ID, dest)
238+
239+
_, err := os.Stat(src)
240+
if err != nil {
241+
return fmt.Errorf("stat %s: %v", src, err)
242+
}
243+
244+
err = c.mkdirOnNode(ctx, node, filepath.Dir(dest))
245+
if err != nil {
246+
return fmt.Errorf("mkdir %s on node %s: %v", filepath.Dir(dest), node.ID, err)
247+
}
248+
249+
args := []string{src}
250+
args = append(args, sshConnectionArgs(node)...)
251+
args[0] = fmt.Sprintf("%s:%s", args[0], dest)
252+
scpCmd := exec.CommandContext(ctx, "scp", args...)
253+
output, err := scpCmd.CombinedOutput()
254+
if err != nil {
255+
return fmt.Errorf("err: %v, output: %s", err, string(output))
256+
}
257+
return nil
258+
}
259+
260+
func (c *Cluster) CopyDirToNode(ctx context.Context, node *node, src, dest string) error {
261+
c.t.Logf("Copying dir %s to node %s at %s", src, node.ID, dest)
262+
263+
_, err := os.Stat(src)
264+
if err != nil {
265+
return fmt.Errorf("stat %s: %v", src, err)
266+
}
267+
268+
err = c.mkdirOnNode(ctx, node, filepath.Dir(dest))
269+
if err != nil {
270+
return fmt.Errorf("mkdir %s on node %s: %v", filepath.Dir(dest), node.ID, err)
271+
}
272+
273+
args := []string{src}
274+
args = append(args, sshConnectionArgs(node)...)
275+
args[0] = fmt.Sprintf("%s:%s", args[0], dest)
276+
scpCmd := exec.CommandContext(ctx, "scp", args...)
277+
output, err := scpCmd.CombinedOutput()
278+
if err != nil {
279+
return fmt.Errorf("err: %v, output: %s", err, string(output))
280+
}
281+
return nil
282+
}
283+
284+
func (c *Cluster) mkdirOnNode(ctx context.Context, node *node, dir string) error {
285+
_, stderr, err := c.runCommandOnNode(ctx, node, []string{"mkdir", "-p", dir}, nil)
286+
if err != nil {
287+
return fmt.Errorf("err: %v, stderr: %s", err, stderr)
288+
}
289+
return nil
290+
}
291+
292+
func sshConnectionArgs(node *node) []string {
293+
if sshUser := os.Getenv("REPLICATEDVM_SSH_USER"); sshUser != "" {
294+
// If ssh user is provided, we can make a direct ssh connection
295+
return []string{fmt.Sprintf("%s@%s", sshUser, node.DirectSSHEndpoint), "-p", strconv.Itoa(node.DirectSSHPort), "-o", "StrictHostKeyChecking=no"}
296+
}
297+
298+
sshDomain := os.Getenv("REPLICATEDVM_SSH_DOMAIN")
299+
if sshDomain == "" {
300+
sshDomain = "replicatedcluster.com"
301+
}
302+
return []string{fmt.Sprintf("%s@%s", node.ID, sshDomain), "-o", "StrictHostKeyChecking=no"}
303+
}
304+
305+
// SetupPlaywright installs necessary dependencies for Playwright testing
306+
func (c *Cluster) SetupPlaywright(ctx context.Context, envs ...map[string]string) error {
307+
c.t.Logf("Setting up Playwright")
308+
309+
// Install Node.js and other dependencies
310+
setupCommands := [][]string{
311+
{"curl", "-fsSL", "https://deb.nodesource.com/setup_16.x", "|", "sudo", "-E", "bash", "-"},
312+
{"sudo", "apt-get", "install", "-y", "nodejs"},
313+
{"npm", "install", "-g", "playwright"},
314+
{"npx", "playwright", "install"},
315+
}
316+
317+
for _, cmd := range setupCommands {
318+
_, stderr, err := c.RunCommandOnNode(ctx, 0, cmd, envs...)
319+
if err != nil {
320+
return fmt.Errorf("run command %q on node %s: %v, stderr: %s", strings.Join(cmd, " "), c.nodes[0].ID, err, stderr)
321+
}
322+
}
323+
324+
return nil
325+
}
326+
327+
// SetupPlaywrightAndRunTest combines setup and test execution
328+
func (c *Cluster) SetupPlaywrightAndRunTest(ctx context.Context, testName string, args ...string) (string, string, error) {
329+
if err := c.SetupPlaywright(ctx); err != nil {
330+
return "", "", err
331+
}
332+
return c.RunPlaywrightTest(ctx, testName, args...)
333+
}
334+
335+
// RunPlaywrightTest executes a Playwright test
336+
func (c *Cluster) RunPlaywrightTest(ctx context.Context, testName string, args ...string) (string, string, error) {
337+
c.t.Logf("Running Playwright test %s", testName)
338+
339+
// Construct the test command
340+
testCmd := append([]string{"npx", "playwright", "test", testName}, args...)
341+
return c.RunCommandOnNode(ctx, 0, testCmd)
342+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/replicatedhq/embedded-cluster/e2e/cluster/cmx"
8+
)
9+
10+
func TestNewCluster(t *testing.T) {
11+
_ = cmx.NewCluster(context.Background(), cmx.ClusterInput{
12+
T: t,
13+
Nodes: 5,
14+
Distribution: "ubuntu",
15+
Version: "22.04",
16+
WithProxy: true,
17+
// AirgapInstallBundlePath: "/tmp/airgap-install-bundle.tar.gz",
18+
// AirgapUpgradeBundlePath: "/tmp/airgap-upgrade-bundle.tar.gz",
19+
})
20+
// defer cluster.Cleanup()
21+
}

0 commit comments

Comments
 (0)