Files
osmedeus/test/e2e/cloud_setup_test.go
T
j3ssie 0269cf4e26 feat: update Next.js build assets and add cloud setup E2E tests
- Update Next.js generated chunk hashes and build IDs reflecting latest dashboard build
- Update CSS stylesheet references in workflow upload page metadata
- Add comprehensive cloud setup E2E test suite (cloud_setup_test.go) with SSH password/key auth, post-command variable expansion, and Ansible integration
- Fix API priority levels to include 'medium' priority in test coverage
- Add agent-sdk test workflows (minimal, config, codex, multi-agent, session variants)
- Update E2E test utilities with runCLIInBase helper for multi-step cloud config tests
- Fix stderr/stdout capture in dependencies_target_types_test assertions
2026-04-04 13:57:34 +08:00

361 lines
13 KiB
Go

package e2e
import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
cloudTestSSHHost = "localhost"
cloudTestSSHPort = "2222"
cloudTestSSHUser = "testuser"
cloudTestSSHPass = "testpass"
)
// startCloudTestSSHServer starts the SSH test container and returns a cleanup func
func startCloudTestSSHServer(t *testing.T) func() {
t.Helper()
composePath := filepath.Join(getProjectRoot(t), "build", "docker", "docker-compose.test.yaml")
cmd := exec.Command("docker-compose", "-f", composePath, "up", "-d", "ssh-server")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("Failed to start SSH container: %v\nOutput: %s", err, output)
}
// Wait for SSH to be ready
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
conn, connErr := net.DialTimeout("tcp", cloudTestSSHHost+":"+cloudTestSSHPort, time.Second)
if connErr == nil {
_ = conn.Close()
// Extra wait for sshd to fully initialize
time.Sleep(2 * time.Second)
return func() {
cmd := exec.Command("docker-compose", "-f", composePath, "down", "-v")
_ = cmd.Run()
}
}
time.Sleep(time.Second)
}
t.Fatal("SSH server did not become ready in time")
return nil
}
// requireSshpass checks that sshpass is installed
func requireSshpass(t *testing.T) {
t.Helper()
if _, err := exec.LookPath("sshpass"); err != nil {
t.Skip("sshpass not installed — skipping password auth test")
}
}
// TestCloudSetup_SSHPassword tests running commands on the SSH container via password auth
func TestCloudSetup_SSHPassword(t *testing.T) {
requireSshpass(t)
cleanup := startCloudTestSSHServer(t)
defer cleanup()
// Run a simple command via sshpass + ssh
sshCmd := fmt.Sprintf(
"sshpass -p '%s' ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p %s %s@%s 'echo CLOUD_SETUP_OK'",
cloudTestSSHPass, cloudTestSSHPort, cloudTestSSHUser, cloudTestSSHHost,
)
out, err := exec.Command("bash", "-c", sshCmd).CombinedOutput()
require.NoError(t, err, "SSH password auth failed: %s", string(out))
assert.Contains(t, string(out), "CLOUD_SETUP_OK")
// Run multiple commands (simulating setup.commands)
commands := []string{
"echo STEP_1_DONE",
"echo STEP_2_DONE",
"whoami",
}
for _, cmd := range commands {
fullCmd := fmt.Sprintf(
"sshpass -p '%s' ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p %s %s@%s '%s'",
cloudTestSSHPass, cloudTestSSHPort, cloudTestSSHUser, cloudTestSSHHost, cmd,
)
out, err := exec.Command("bash", "-c", fullCmd).CombinedOutput()
require.NoError(t, err, "Command '%s' failed: %s", cmd, string(out))
}
}
// TestCloudSetup_SSHKey tests running commands on the SSH container via key auth
func TestCloudSetup_SSHKey(t *testing.T) {
cleanup := startCloudTestSSHServer(t)
defer cleanup()
// Generate a temp SSH key pair
tmpDir := t.TempDir()
keyPath := filepath.Join(tmpDir, "test_key")
cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", keyPath, "-N", "", "-q")
require.NoError(t, cmd.Run(), "Failed to generate SSH key")
pubKeyBytes, err := os.ReadFile(keyPath + ".pub")
require.NoError(t, err)
pubKey := strings.TrimSpace(string(pubKeyBytes))
// Add the public key to the container's authorized_keys via password auth
requireSshpass(t)
addKeyCmd := fmt.Sprintf(
"sshpass -p '%s' ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p %s %s@%s 'mkdir -p ~/.ssh && echo \"%s\" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'",
cloudTestSSHPass, cloudTestSSHPort, cloudTestSSHUser, cloudTestSSHHost, pubKey,
)
out, err := exec.Command("bash", "-c", addKeyCmd).CombinedOutput()
require.NoError(t, err, "Failed to add key to container: %s", string(out))
// Now test key-based auth
sshCmd := fmt.Sprintf(
"ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -i %s -p %s %s@%s 'echo KEY_AUTH_OK'",
keyPath, cloudTestSSHPort, cloudTestSSHUser, cloudTestSSHHost,
)
out, err = exec.Command("bash", "-c", sshCmd).CombinedOutput()
require.NoError(t, err, "SSH key auth failed: %s", string(out))
assert.Contains(t, string(out), "KEY_AUTH_OK")
}
// TestCloudSetup_PostCommandVars tests that template variables in post_commands are expanded
func TestCloudSetup_PostCommandVars(t *testing.T) {
requireSshpass(t)
cleanup := startCloudTestSSHServer(t)
defer cleanup()
vars := map[string]string{
"public_ip": "10.0.0.1",
"worker_name": "osmw-test-0",
"infra_id": "cloud-test-123",
"provider": "remote-adhoc",
"ssh_user": cloudTestSSHUser,
"index": "0",
}
// Test variable expansion
template := "echo IP={{public_ip}} NAME={{worker_name}} INFRA={{infra_id}} PROVIDER={{provider}}"
expanded := template
for k, v := range vars {
expanded = strings.ReplaceAll(expanded, "{{"+k+"}}", v)
}
assert.Contains(t, expanded, "IP=10.0.0.1")
assert.Contains(t, expanded, "NAME=osmw-test-0")
assert.Contains(t, expanded, "INFRA=cloud-test-123")
assert.Contains(t, expanded, "PROVIDER=remote-adhoc")
// Run the expanded command on the SSH container
sshCmd := fmt.Sprintf(
"sshpass -p '%s' ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p %s %s@%s '%s'",
cloudTestSSHPass, cloudTestSSHPort, cloudTestSSHUser, cloudTestSSHHost, expanded,
)
out, err := exec.Command("bash", "-c", sshCmd).CombinedOutput()
require.NoError(t, err, "Post-command failed: %s", string(out))
assert.Contains(t, string(out), "IP=10.0.0.1")
assert.Contains(t, string(out), "NAME=osmw-test-0")
}
// TestCloudSetup_Ansible tests running an ansible playbook against the SSH container
func TestCloudSetup_Ansible(t *testing.T) {
// Check ansible is installed
if _, err := exec.LookPath("ansible-playbook"); err != nil {
t.Skip("ansible-playbook not installed — skipping ansible test")
}
cleanup := startCloudTestSSHServer(t)
defer cleanup()
tmpDir := t.TempDir()
// Create a simple test playbook (use raw module — no Python needed in container)
playbook := `---
- name: Test Cloud Setup
hosts: osmedeus_workers
gather_facts: no
tasks:
- name: Echo test
raw: echo ANSIBLE_OK
register: result
- name: Show result
debug:
msg: "{{ result.stdout }}"
`
playbookPath := filepath.Join(tmpDir, "test-playbook.yaml")
require.NoError(t, os.WriteFile(playbookPath, []byte(playbook), 0644))
// Create inventory with password auth
inventory := fmt.Sprintf(`[osmedeus_workers]
%s ansible_user=%s ansible_ssh_pass=%s ansible_port=%s
[osmedeus_workers:vars]
ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR'
`, cloudTestSSHHost, cloudTestSSHUser, cloudTestSSHPass, cloudTestSSHPort)
inventoryPath := filepath.Join(tmpDir, "inventory.ini")
require.NoError(t, os.WriteFile(inventoryPath, []byte(inventory), 0644))
// Run ansible-playbook
cmd := exec.Command("ansible-playbook", "-i", inventoryPath, playbookPath)
out, err := cmd.CombinedOutput()
t.Logf("Ansible output:\n%s", string(out))
require.NoError(t, err, "Ansible playbook failed: %s", string(out))
assert.Contains(t, string(out), "ANSIBLE_OK")
}
// sshReadFile reads a file from the test SSH container via sshpass
func sshReadFile(t *testing.T, path string) string {
t.Helper()
sshCmd := fmt.Sprintf(
"sshpass -p '%s' ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p %s %s@%s 'cat %s'",
cloudTestSSHPass, cloudTestSSHPort, cloudTestSSHUser, cloudTestSSHHost, path,
)
out, err := exec.Command("bash", "-c", sshCmd).CombinedOutput()
require.NoError(t, err, "Failed to read %s via SSH: %s", path, string(out))
return strings.TrimSpace(string(out))
}
// TestCloudSetup_FullCLIFlow tests the full cloud setup CLI flow:
// configure cloud settings, run `osmedeus cloud setup`, verify results via SSH
func TestCloudSetup_FullCLIFlow(t *testing.T) {
if testing.Short() {
t.Skip("skipping cloud setup E2E test in short mode")
}
requireSshpass(t)
cleanup := startCloudTestSSHServer(t)
defer cleanup()
log := NewTestLogger(t)
baseDir := t.TempDir()
// Configure cloud settings
log.Step("Configuring cloud settings")
configSets := [][]string{
{"cloud", "config", "set", "ssh.password", cloudTestSSHPass},
{"cloud", "config", "set", "ssh.user", cloudTestSSHUser},
{"cloud", "config", "set", "ssh.port", cloudTestSSHPort},
{"cloud", "config", "set", "setup.commands.add", "echo SETUP_MARKER > /tmp/setup-ok"},
{"cloud", "config", "set", "setup.commands.add", "echo STEP_TWO >> /tmp/setup-ok"},
{"cloud", "config", "set", "setup.post_commands.add", "echo IP={{public_ip}} > /tmp/post-ok"},
}
for _, args := range configSets {
stdout, _, err := runCLIInBase(t, log, baseDir, args...)
require.NoError(t, err, "config set failed: %s", stdout)
}
// Run cloud setup against the SSH container
log.Step("Running cloud setup")
stdout, _, err := runCLIInBase(t, log, baseDir, "cloud", "setup", cloudTestSSHHost)
require.NoError(t, err, "cloud setup failed: %s", stdout)
assert.Contains(t, stdout, "Setup complete")
// Verify setup commands ran
log.Step("Verifying setup results")
marker := sshReadFile(t, "/tmp/setup-ok")
assert.Contains(t, marker, "SETUP_MARKER")
assert.Contains(t, marker, "STEP_TWO")
// Verify post-commands ran with variable expansion
postMarker := sshReadFile(t, "/tmp/post-ok")
assert.Contains(t, postMarker, "IP="+cloudTestSSHHost)
log.Success("Full CLI flow cloud setup works correctly")
}
// TestCloudSetup_ClearCommandsThenSetup tests that clearing setup commands works
// and that cloud setup handles empty command lists gracefully
func TestCloudSetup_ClearCommandsThenSetup(t *testing.T) {
if testing.Short() {
t.Skip("skipping cloud setup E2E test in short mode")
}
requireSshpass(t)
cleanup := startCloudTestSSHServer(t)
defer cleanup()
log := NewTestLogger(t)
baseDir := t.TempDir()
// Add then clear setup commands
log.Step("Adding and clearing setup commands")
_, _, err := runCLIInBase(t, log, baseDir, "cloud", "config", "set", "ssh.password", cloudTestSSHPass)
require.NoError(t, err)
_, _, err = runCLIInBase(t, log, baseDir, "cloud", "config", "set", "ssh.user", cloudTestSSHUser)
require.NoError(t, err)
_, _, err = runCLIInBase(t, log, baseDir, "cloud", "config", "set", "ssh.port", cloudTestSSHPort)
require.NoError(t, err)
_, _, err = runCLIInBase(t, log, baseDir, "cloud", "config", "set", "setup.commands.add", "echo SHOULD_NOT_RUN")
require.NoError(t, err)
_, _, err = runCLIInBase(t, log, baseDir, "cloud", "config", "set", "setup.commands.clear")
require.NoError(t, err)
// Run cloud setup — should report no commands
log.Step("Running cloud setup with cleared commands")
stdout, _, err := runCLIInBase(t, log, baseDir, "cloud", "setup", cloudTestSSHHost)
require.NoError(t, err, "cloud setup failed: %s", stdout)
assert.Contains(t, stdout, "No setup commands configured")
log.Success("Clear commands + setup works correctly")
}
// TestCloudSetup_AnsibleCLIFlow tests running cloud setup with --ansible flag via the CLI
func TestCloudSetup_AnsibleCLIFlow(t *testing.T) {
if testing.Short() {
t.Skip("skipping cloud setup E2E test in short mode")
}
if _, err := exec.LookPath("ansible-playbook"); err != nil {
t.Skip("ansible-playbook not installed — skipping ansible CLI test")
}
requireSshpass(t)
cleanup := startCloudTestSSHServer(t)
defer cleanup()
log := NewTestLogger(t)
baseDir := t.TempDir()
// Create a test playbook that writes a marker file
playbookDir := filepath.Join(baseDir, "cloud-infra")
require.NoError(t, os.MkdirAll(playbookDir, 0755))
playbook := `---
- name: Test Cloud Setup via CLI
hosts: osmedeus_workers
gather_facts: no
tasks:
- name: Write ansible marker
raw: echo ANSIBLE_CLI_OK > /tmp/ansible-marker
`
playbookPath := filepath.Join(playbookDir, "setup-playbook.yaml")
require.NoError(t, os.WriteFile(playbookPath, []byte(playbook), 0644))
// Configure cloud settings with ansible
log.Step("Configuring cloud settings for ansible")
configSets := [][]string{
{"cloud", "config", "set", "ssh.password", cloudTestSSHPass},
{"cloud", "config", "set", "ssh.user", cloudTestSSHUser},
{"cloud", "config", "set", "ssh.port", cloudTestSSHPort},
{"cloud", "config", "set", "setup.ansible.enabled", "true"},
{"cloud", "config", "set", "setup.ansible.playbook_path", playbookPath},
}
for _, args := range configSets {
_, _, err := runCLIInBase(t, log, baseDir, args...)
require.NoError(t, err)
}
// Run cloud setup with ansible
log.Step("Running cloud setup with ansible")
stdout, _, err := runCLIInBase(t, log, baseDir, "cloud", "setup", cloudTestSSHHost, "--ansible")
require.NoError(t, err, "cloud setup --ansible failed: %s", stdout)
// Verify ansible playbook ran
log.Step("Verifying ansible results")
marker := sshReadFile(t, "/tmp/ansible-marker")
assert.Contains(t, marker, "ANSIBLE_CLI_OK")
log.Success("Ansible CLI flow cloud setup works correctly")
}