mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-25 00:52:28 +02:00
- 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
361 lines
13 KiB
Go
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")
|
|
}
|