Files
osmedeus/test/e2e/ssh_test.go
T

245 lines
7.1 KiB
Go

package e2e
import (
"net"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testSSHPort = "2222"
testSSHUser = "testuser"
testSSHPassword = "testpass"
)
// getSSHDockerComposePath returns the path to the SSH test docker-compose file
func getSSHDockerComposePath(t *testing.T) string {
t.Helper()
return filepath.Join(getProjectRoot(t), "build", "docker", "docker-compose.test.yaml")
}
// startSSHServer starts the SSH container using docker-compose
func startSSHServer(t *testing.T, log *TestLogger) func() {
t.Helper()
composePath := getSSHDockerComposePath(t)
log.Info("Starting SSH server container from %s", composePath)
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
waitForSSH(t, log, testSSHPort, 30*time.Second)
return func() {
log.Info("Stopping SSH server container")
cmd := exec.Command("docker-compose", "-f", composePath, "down", "-v")
_ = cmd.Run()
}
}
// waitForSSH waits for SSH server to accept connections
// The linuxserver/openssh-server container may open port before SSH daemon is ready
func waitForSSH(t *testing.T, log *TestLogger, port string, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
log.Info("Waiting for SSH on port %s...", port)
// First, wait for port to be open
for time.Now().Before(deadline) {
conn, err := net.DialTimeout("tcp", "localhost:"+port, time.Second)
if err == nil {
_ = conn.Close()
break
}
time.Sleep(time.Second)
}
// Give SSH daemon extra time to fully initialize after port is open
log.Info("Port open, waiting for SSH daemon to initialize...")
time.Sleep(5 * time.Second)
// Verify SSH connection works by attempting a real connection
for time.Now().Before(deadline) {
conn, err := net.DialTimeout("tcp", "localhost:"+port, 2*time.Second)
if err == nil {
// Try to read the SSH banner to confirm daemon is ready
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
buf := make([]byte, 256)
n, err := conn.Read(buf)
_ = conn.Close()
if err == nil && n > 0 && string(buf[:3]) == "SSH" {
log.Success("SSH server is ready")
return
}
}
time.Sleep(time.Second)
}
t.Fatalf("SSH server not ready after %v", timeout)
}
// TestSSH_ModuleRunner tests the module-level SSH runner (runner: ssh)
// This test starts a real Docker SSH container and executes a workflow
// that runs all steps on the remote SSH server
func TestSSH_ModuleRunner(t *testing.T) {
if testing.Short() {
t.Skip("skipping SSH e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Testing module-level SSH runner")
// Start SSH server
cleanupSSH := startSSHServer(t, log)
defer cleanupSSH()
// Run run with SSH runner workflow
workflowPath := getTestdataPath(t)
log.Info("Using workflow path: %s", workflowPath)
stdout, stderr, err := runCLIWithLog(t, log, "run",
"-m", "test-ssh-runner",
"-t", "ssh-e2e-test",
"-F", workflowPath)
require.NoError(t, err, "SSH runner run failed: %s", stderr)
// Check for successful completion - the CLI doesn't print raw command output
assert.Contains(t, stdout, "check-remote")
assert.Contains(t, stdout, "Status: completed")
log.Success("Module-level SSH runner test passed")
}
// TestSSH_StepRunner tests the step-level SSH runner (step_runner: ssh)
// This test uses the remote-bash step type with SSH runner configuration
// to execute individual steps on a remote SSH server
func TestSSH_StepRunner(t *testing.T) {
if testing.Short() {
t.Skip("skipping SSH e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Testing step-level SSH runner (remote-bash)")
// Start SSH server
cleanupSSH := startSSHServer(t, log)
defer cleanupSSH()
// Run run with remote-bash SSH workflow
workflowPath := getTestdataPath(t)
log.Info("Using workflow path: %s", workflowPath)
stdout, stderr, err := runCLIWithLog(t, log, "run",
"-m", "test-remote-bash-ssh",
"-t", "remote-bash-e2e-test",
"-p", "ssh_host=localhost",
"-p", "ssh_user=testuser",
"-p", "ssh_password=testpass",
"-F", workflowPath)
require.NoError(t, err, "remote-bash SSH run failed: %s", stderr)
// Check for successful completion - the CLI doesn't print raw command output
assert.Contains(t, stdout, "check-ssh-connection")
assert.Contains(t, stdout, "Status: completed")
log.Success("Step-level SSH runner test passed")
}
// TestSSH_FullWorkflow is a comprehensive E2E test that verifies:
// 1. SSH container startup
// 2. Module-level SSH runner execution
// 3. Step-level SSH runner execution
// 4. Command output verification
// 5. Proper cleanup
func TestSSH_FullWorkflow(t *testing.T) {
if testing.Short() {
t.Skip("skipping SSH e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Running full SSH workflow e2e test")
// Step 1: Start SSH server
log.Step("Step 1: Starting SSH container")
cleanupSSH := startSSHServer(t, log)
defer cleanupSSH()
workflowPath := getTestdataPath(t)
// Step 2: Test module-level SSH runner
log.Step("Step 2: Testing module-level SSH runner")
stdout, stderr, err := runCLIWithLog(t, log, "run",
"-m", "test-ssh-runner",
"-t", "full-ssh-test",
"-F", workflowPath)
require.NoError(t, err, "Module-level SSH test failed: %s", stderr)
assert.Contains(t, stdout, "Status: completed", "Expected successful completion")
assert.Contains(t, stdout, "check-remote")
// Step 3: Test step-level SSH runner
log.Step("Step 3: Testing step-level SSH runner")
stdout, stderr, err = runCLIWithLog(t, log, "run",
"-m", "test-remote-bash-ssh",
"-t", "full-remote-bash-test",
"-p", "ssh_host=localhost",
"-p", "ssh_user=testuser",
"-p", "ssh_password=testpass",
"-F", workflowPath)
require.NoError(t, err, "Step-level SSH test failed: %s", stderr)
assert.Contains(t, stdout, "Status: completed", "Expected successful completion")
log.Success("Full SSH workflow e2e test passed!")
}
// TestSSH_DryRun tests SSH runner in dry-run mode
// NOTE: SSH runner still attempts connection in dry-run mode (runner.Setup is called)
// so this test requires the SSH container to be running
func TestSSH_DryRun(t *testing.T) {
if testing.Short() {
t.Skip("skipping SSH e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Testing SSH runner in dry-run mode")
// Start SSH server (dry-run still connects for validation)
cleanupSSH := startSSHServer(t, log)
defer cleanupSSH()
workflowPath := getTestdataPath(t)
log.Info("Using workflow path: %s", workflowPath)
// Module-level SSH runner dry-run
stdout, _, err := runCLIWithLog(t, log, "run",
"-m", "test-ssh-runner",
"-t", "ssh-dryrun-test",
"--dry-run",
"-F", workflowPath)
require.NoError(t, err)
assert.Contains(t, stdout, "DRY-RUN")
// Step-level SSH runner dry-run
stdout, _, err = runCLIWithLog(t, log, "run",
"-m", "test-remote-bash-ssh",
"-t", "remote-bash-dryrun-test",
"--dry-run",
"-F", workflowPath)
require.NoError(t, err)
assert.Contains(t, stdout, "DRY-RUN")
log.Success("SSH dry-run test passed")
}