Add Azure as a managed provider for direct and brokered Crabbox leases. - provision Azure Linux VMs with cloud-init, spot fallback, shared network adoption, and per-lease cleanup - provision native Azure Windows VMs with VM Agent bootstrap and SSH/sync/run support - add Azure broker support in the Cloudflare Worker, provider config, docs, and tests - fix async Azure delete handling so successful 202 delete LROs do not refetch deleted resources - keep Go core coverage above the CI threshold Verified with CI plus live Azure Linux and native Windows leases. Co-authored-by: Jonathan Moss <2729151+jwmoss@users.noreply.github.com>
281 lines
9.9 KiB
Go
281 lines
9.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCloudInitUsesRetryingBootstrap(t *testing.T) {
|
|
got := cloudInit(baseConfig(), "ssh-ed25519 test")
|
|
for _, want := range []string{
|
|
"package_update: false",
|
|
"bash -euxo pipefail <<'BOOT'",
|
|
"Acquire::Retries \"8\";",
|
|
"retry apt-get update",
|
|
"retry apt-get install -y --no-install-recommends openssh-server ca-certificates curl git rsync jq",
|
|
"curl --version >/dev/null",
|
|
"test -f /var/lib/crabbox/bootstrapped",
|
|
"test -w /work/crabbox",
|
|
" Port 2222\n Port 22",
|
|
"systemctl enable ssh || true",
|
|
"timeout 30s systemctl restart ssh || timeout 30s systemctl restart ssh.socket || true",
|
|
"touch /var/lib/crabbox/bootstrapped",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("cloudInit() missing %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(got, "\npackages:\n") {
|
|
t.Fatal("cloudInit() must not use cloud-init's one-shot packages module")
|
|
}
|
|
if strings.Contains(got, "systemctl enable --now ssh") {
|
|
t.Fatal("cloudInit() must not use blocking systemctl enable --now ssh")
|
|
}
|
|
for _, notWant := range []string{"go version", "golang-go", "go.dev/dl/go", "/usr/local/go", "node --version", "pnpm --version", "docker --version", "build-essential", "docker.io", "corepack"} {
|
|
if strings.Contains(got, notWant) {
|
|
t.Fatalf("cloudInit() should not install project language runtime %q", notWant)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloudInitStartsSSHBeforeOptionalDesktopBootstrap(t *testing.T) {
|
|
cfg := baseConfig()
|
|
cfg.Desktop = true
|
|
got := cloudInit(cfg, "ssh-ed25519 test")
|
|
sshIndex := strings.Index(got, "timeout 30s systemctl restart ssh")
|
|
desktopIndex := strings.Index(got, "retry apt-get install -y --no-install-recommends xvfb")
|
|
bootstrappedIndex := strings.Index(got, "touch /var/lib/crabbox/bootstrapped")
|
|
if sshIndex < 0 || desktopIndex < 0 || bootstrappedIndex < 0 {
|
|
t.Fatalf("cloudInit(desktop) missing expected bootstrap markers")
|
|
}
|
|
if sshIndex > desktopIndex {
|
|
t.Fatalf("ssh should start before slow desktop bootstrap")
|
|
}
|
|
if bootstrappedIndex < desktopIndex {
|
|
t.Fatalf("bootstrapped marker should stay after desktop bootstrap")
|
|
}
|
|
}
|
|
|
|
func TestCloudInitDesktopProfile(t *testing.T) {
|
|
cfg := baseConfig()
|
|
cfg.Desktop = true
|
|
got := cloudInit(cfg, "ssh-ed25519 test")
|
|
for _, want := range []string{
|
|
"xvfb xfce4-session xfwm4 xfce4-panel xfdesktop4 xfce4-terminal",
|
|
"xfconf xfce4-settings x11vnc xauth dbus-x11",
|
|
"x11-xserver-utils xterm scrot ffmpeg xdotool wmctrl xclip xsel",
|
|
"/etc/systemd/system/crabbox-xvfb.service",
|
|
"/etc/systemd/system/crabbox-desktop.service",
|
|
"/usr/local/bin/crabbox-desktop-session",
|
|
"/etc/systemd/system/crabbox-desktop-session.service",
|
|
"/etc/systemd/system/crabbox-x11vnc.service",
|
|
"ExecStart=/usr/bin/startxfce4",
|
|
"systemctl is-active --quiet crabbox-desktop.service",
|
|
"systemctl is-active --quiet crabbox-desktop-session.service",
|
|
"xsetroot -solid '#20242b'",
|
|
"xfce4-terminal --title='Crabbox Desktop'",
|
|
"xterm -title 'Crabbox Desktop'",
|
|
"(umask 077 && openssl rand -base64 18 > /var/lib/crabbox/vnc.password)",
|
|
"x11vnc -storepasswd",
|
|
"-rfbauth /var/lib/crabbox/vnc.pass",
|
|
"ss -ltn | grep -q '127.0.0.1:5900'",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("cloudInit(desktop) missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloudInitBrowserProfile(t *testing.T) {
|
|
cfg := baseConfig()
|
|
cfg.Browser = true
|
|
got := cloudInit(cfg, "ssh-ed25519 test")
|
|
for _, want := range []string{
|
|
"gnupg build-essential python3",
|
|
"https://dl.google.com/linux/linux_signing_key.pub",
|
|
"chmod 0644 /etc/apt/trusted.gpg.d/google.asc",
|
|
"https://dl.google.com/linux/chrome/deb/",
|
|
"google-chrome-stable",
|
|
"apt-cache show chromium",
|
|
"apt-cache show chromium-browser",
|
|
"/etc/opt/chrome/policies/managed/crabbox.json",
|
|
"/usr/local/bin/crabbox-browser",
|
|
"--no-first-run --no-default-browser-check --disable-default-apps --window-size=1500,900 --window-position=80,80",
|
|
"/var/lib/crabbox/browser.env",
|
|
"test -x \"$BROWSER\"",
|
|
"\"$BROWSER\" --version >/dev/null",
|
|
"printf '%s\\n' '{\"DefaultBrowserSettingEnabled\":false,\"MetricsReportingEnabled\":false,\"PromotionalTabsEnabled\":false}' > /etc/opt/chrome/policies/managed/crabbox.json",
|
|
"printf '%s\\n' '#!/bin/sh' \"exec \\\"$browser_path\\\" --no-first-run --no-default-browser-check --disable-default-apps --window-size=1500,900 --window-position=80,80 \\\"\\$@\\\"\" > \"$browser_wrapper\"",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("cloudInit(browser) missing %q", want)
|
|
}
|
|
}
|
|
for _, notWant := range []string{
|
|
"<<'EOF'",
|
|
"<<EOF",
|
|
"\nEOF",
|
|
} {
|
|
if strings.Contains(got, notWant) {
|
|
t.Fatalf("cloudInit(browser) contains browser heredoc content %q", notWant)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloudInitCodeProfile(t *testing.T) {
|
|
cfg := baseConfig()
|
|
cfg.Code = true
|
|
got := cloudInit(cfg, "ssh-ed25519 test")
|
|
for _, want := range []string{
|
|
"https://code-server.dev/install.sh",
|
|
"env HOME=/root",
|
|
"--method=standalone --prefix=/usr/local",
|
|
"/usr/local/bin/code-server --version >/dev/null",
|
|
"test -x /usr/local/bin/code-server",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("cloudInit(code) missing %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(cloudInit(baseConfig(), "ssh-ed25519 test"), "code-server") {
|
|
t.Fatal("cloudInit should not install code-server by default")
|
|
}
|
|
}
|
|
|
|
func TestCloudInitTailscaleProfile(t *testing.T) {
|
|
cfg := baseConfig()
|
|
cfg.SSHUser = "runner"
|
|
cfg.Tailscale.Enabled = true
|
|
cfg.Tailscale.AuthKey = "tskey-secret"
|
|
cfg.Tailscale.Hostname = "crabbox-blue-lobster"
|
|
cfg.Tailscale.Tags = []string{"tag:crabbox"}
|
|
cfg.Tailscale.ExitNode = "mac-studio.tailnet.ts.net"
|
|
cfg.Tailscale.ExitNodeAllowLANAccess = true
|
|
got := cloudInit(cfg, "ssh-ed25519 test")
|
|
for _, want := range []string{
|
|
"https://tailscale.com/install.sh",
|
|
"install -d -m 0750 -o 'runner' -g 'runner' /var/lib/crabbox",
|
|
"tailscale up --auth-key=\"$TS_AUTHKEY\" --hostname='crabbox-blue-lobster' --advertise-tags='tag:crabbox' --exit-node='mac-studio.tailnet.ts.net' --exit-node-allow-lan-access",
|
|
"printf '%s\\n' 'crabbox-blue-lobster' > /var/lib/crabbox/tailscale-hostname",
|
|
"printf '%s\\n' 'mac-studio.tailnet.ts.net' > /var/lib/crabbox/tailscale-exit-node",
|
|
"printf '%s\\n' 'true' > /var/lib/crabbox/tailscale-exit-node-allow-lan-access",
|
|
"chown 'runner:runner' /var/lib/crabbox/tailscale-* || true",
|
|
"test -s /var/lib/crabbox/tailscale-ipv4",
|
|
"grep -Eq '^100\\.' /var/lib/crabbox/tailscale-ipv4",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("cloudInit(tailscale) missing %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(cloudInit(baseConfig(), "ssh-ed25519 test"), "tailscale up") {
|
|
t.Fatal("cloudInit should not install Tailscale by default")
|
|
}
|
|
}
|
|
|
|
func TestAWSUserDataWindowsProfile(t *testing.T) {
|
|
cfg := baseConfig()
|
|
cfg.Provider = "aws"
|
|
cfg.TargetOS = targetWindows
|
|
cfg.WindowsMode = windowsModeNormal
|
|
cfg.WorkRoot = `C:\crabbox`
|
|
userData := awsUserData(cfg, "ssh-ed25519 test")
|
|
if !strings.Contains(userData, "version: 1.1") || !strings.Contains(userData, "task: enableOpenSsh") {
|
|
t.Fatalf("windows user data should enable EC2Launch OpenSSH:\n%s", userData)
|
|
}
|
|
got := windowsBootstrapPowerShell(cfg, "ssh-ed25519 test")
|
|
for _, want := range []string{
|
|
"OpenSSH-Win64.zip",
|
|
"install-sshd.ps1",
|
|
"administrators_authorized_keys",
|
|
"Match Group administrators",
|
|
"$sshPorts = @('2222', '22')",
|
|
"sshd_config",
|
|
"Port $port",
|
|
"crabbox-sshd-$port",
|
|
"Git-2.52.0-64-bit.exe",
|
|
"tightvnc-2.8.85-gpl-setup-64bit.msi",
|
|
"VALUE_OF_PASSWORD=$vncPassword",
|
|
"VALUE_OF_ALLOWLOOPBACK=1",
|
|
"CrabboxUserVNC",
|
|
"crabbox-user-vnc.cmd",
|
|
"start-user-vnc.ps1",
|
|
"Set-TightVNCBinaryValue",
|
|
`reg.exe add "HKCU\Software\TightVNC\Server"`,
|
|
`$hex = -join ($bytes | ForEach-Object { $_.ToString("X2") })`,
|
|
"-run",
|
|
"Set-Service -StartupType Disabled",
|
|
"New-CrabboxPassword",
|
|
"${userSID}:F",
|
|
`C:\ProgramData\crabbox\vnc.password`,
|
|
`C:\ProgramData\crabbox\windows.username`,
|
|
"AutoAdminLogon",
|
|
"Restart-Computer -Force",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("windows user data missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAWSUserDataWindowsWSL2Profile(t *testing.T) {
|
|
cfg := baseConfig()
|
|
cfg.Provider = "aws"
|
|
cfg.TargetOS = targetWindows
|
|
cfg.WindowsMode = windowsModeWSL2
|
|
cfg.WorkRoot = `/work/crabbox`
|
|
got := windowsBootstrapPowerShell(cfg, "ssh-ed25519 test")
|
|
for _, want := range []string{
|
|
"$wslMode = $true",
|
|
"Microsoft-Windows-Subsystem-Linux",
|
|
"VirtualMachinePlatform",
|
|
"HypervisorPlatform",
|
|
"bcdedit.exe /set hypervisorlaunchtype auto",
|
|
"wsl.exe --update --web-download",
|
|
"wsl.exe --set-default-version 2",
|
|
ubuntuWSLRootFSURL,
|
|
"$wslRootfsMinBytes = 100 * 1024 * 1024",
|
|
`$wslRootfsDownload = "$wslRootfs.download"`,
|
|
"Remove-Item -Force -LiteralPath $wslRootfs",
|
|
"Remove-Item -Force -LiteralPath $wslRootfsDownload",
|
|
"curl.exe -fL --retry 8",
|
|
"downloaded WSL rootfs is incomplete",
|
|
"Move-Item -Force -LiteralPath $wslRootfsDownload -Destination $wslRootfs",
|
|
"wsl.exe --import $wslDistro $wslRoot $wslRootfs --version 2",
|
|
"wsl.exe --set-default $wslDistro",
|
|
`$wslSetup = "C:\ProgramData\crabbox\wsl\linux-setup.sh"`,
|
|
"WriteAllText($wslSetup",
|
|
"wsl.exe -d $wslDistro --user root --exec bash /mnt/c/ProgramData/crabbox/wsl/linux-setup.sh",
|
|
"apt-get install -y --no-install-recommends ca-certificates curl git rsync jq",
|
|
"cat >/usr/local/bin/crabbox-ready",
|
|
`test -w '/work/crabbox'`,
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("windows WSL2 bootstrap missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAWSUserDataMacOSProfile(t *testing.T) {
|
|
cfg := baseConfig()
|
|
cfg.Provider = "aws"
|
|
cfg.TargetOS = targetMacOS
|
|
cfg.SSHUser = "ec2-user"
|
|
cfg.WorkRoot = defaultMacOSWorkRoot
|
|
got := awsUserData(cfg, "ssh-ed25519 test")
|
|
for _, want := range []string{
|
|
"#!/bin/bash",
|
|
defaultMacOSWorkRoot,
|
|
"/var/db/crabbox/vnc.password",
|
|
"set +o pipefail",
|
|
"set -o pipefail",
|
|
"failed to generate vnc password",
|
|
"com.apple.screensharing",
|
|
"/usr/local/bin/crabbox-ready",
|
|
"nc -z 127.0.0.1 5900",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("macOS user data missing %q", want)
|
|
}
|
|
}
|
|
}
|