643 lines
21 KiB
Go
643 lines
21 KiB
Go
package cli
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func clearConfigEnv(t *testing.T) {
|
|
t.Helper()
|
|
for _, key := range []string{
|
|
"CRABBOX_COORDINATOR",
|
|
"CRABBOX_COORDINATOR_TOKEN",
|
|
"CRABBOX_COORDINATOR_ADMIN_TOKEN",
|
|
"CRABBOX_ADMIN_TOKEN",
|
|
"CRABBOX_NETWORK",
|
|
"CRABBOX_TAILSCALE",
|
|
"CRABBOX_TAILSCALE_TAGS",
|
|
"CRABBOX_TAILSCALE_HOSTNAME_TEMPLATE",
|
|
"CRABBOX_TAILSCALE_AUTH_KEY_ENV",
|
|
"CRABBOX_TAILSCALE_AUTH_KEY",
|
|
"CRABBOX_TAILSCALE_EXIT_NODE",
|
|
"CRABBOX_TAILSCALE_EXIT_NODE_ALLOW_LAN_ACCESS",
|
|
"CRABBOX_ACCESS_CLIENT_ID",
|
|
"CRABBOX_ACCESS_CLIENT_SECRET",
|
|
"CRABBOX_ACCESS_TOKEN",
|
|
"CF_ACCESS_CLIENT_ID",
|
|
"CF_ACCESS_CLIENT_SECRET",
|
|
"CF_ACCESS_TOKEN",
|
|
"CRABBOX_DAYTONA_API_KEY",
|
|
"DAYTONA_API_KEY",
|
|
"CRABBOX_DAYTONA_JWT_TOKEN",
|
|
"DAYTONA_JWT_TOKEN",
|
|
"CRABBOX_DAYTONA_ORGANIZATION_ID",
|
|
"DAYTONA_ORGANIZATION_ID",
|
|
"CRABBOX_DAYTONA_API_URL",
|
|
"DAYTONA_API_URL",
|
|
"CRABBOX_DAYTONA_SNAPSHOT",
|
|
"DAYTONA_SNAPSHOT",
|
|
"CRABBOX_DAYTONA_TARGET",
|
|
"DAYTONA_TARGET",
|
|
"CRABBOX_DAYTONA_USER",
|
|
"CRABBOX_DAYTONA_WORK_ROOT",
|
|
"CRABBOX_DAYTONA_SSH_GATEWAY_HOST",
|
|
"CRABBOX_DAYTONA_SSH_ACCESS_MINUTES",
|
|
"CRABBOX_ISLO_API_KEY",
|
|
"ISLO_API_KEY",
|
|
"CRABBOX_ISLO_BASE_URL",
|
|
"ISLO_BASE_URL",
|
|
"CRABBOX_ISLO_IMAGE",
|
|
"CRABBOX_ISLO_WORKDIR",
|
|
"CRABBOX_ISLO_GATEWAY_PROFILE",
|
|
"CRABBOX_ISLO_SNAPSHOT_NAME",
|
|
"CRABBOX_ISLO_VCPUS",
|
|
"CRABBOX_ISLO_MEMORY_MB",
|
|
"CRABBOX_ISLO_DISK_GB",
|
|
} {
|
|
t.Setenv(key, "")
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigFromUserFile(t *testing.T) {
|
|
clearConfigEnv(t)
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("CRABBOX_CONFIG", "")
|
|
t.Setenv("CRABBOX_PROVIDER", "")
|
|
t.Setenv("CRABBOX_DEFAULT_CLASS", "")
|
|
path := userConfigPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(`broker:
|
|
url: https://crabbox.example.test
|
|
token: secret
|
|
adminToken: admin-secret
|
|
provider: aws
|
|
access:
|
|
clientId: access-client
|
|
clientSecret: access-secret
|
|
token: access-jwt
|
|
class: standard
|
|
target: windows
|
|
windows:
|
|
mode: wsl2
|
|
lease:
|
|
ttl: 2h
|
|
idleTimeout: 45m
|
|
aws:
|
|
region: eu-west-1
|
|
rootGB: 800
|
|
sshCIDRs:
|
|
- 198.51.100.7/32
|
|
sync:
|
|
checksum: true
|
|
gitSeed: false
|
|
baseRef: trunk
|
|
timeout: 30m
|
|
warnFiles: 100
|
|
warnBytes: 200
|
|
failFiles: 300
|
|
failBytes: 400
|
|
allowLarge: true
|
|
exclude:
|
|
- .artifacts
|
|
- tmp
|
|
env:
|
|
allow:
|
|
- CI
|
|
- NODE_OPTIONS
|
|
- CUSTOM_*
|
|
capacity:
|
|
market: spot
|
|
strategy: most-available
|
|
fallback: on-demand-after-120s
|
|
hints: false
|
|
regions:
|
|
- eu-west-1
|
|
actions:
|
|
repo: openclaw/crabbox
|
|
workflow: .github/workflows/crabbox.yml
|
|
job: hydrate
|
|
ref: main
|
|
fields:
|
|
- crabbox_docker_cache=true
|
|
- crabbox_prepare_images=1
|
|
runnerLabels:
|
|
- crabbox
|
|
- linux-large
|
|
runnerVersion: latest
|
|
ephemeral: false
|
|
blacksmith:
|
|
org: openclaw
|
|
workflow: .github/workflows/blacksmith-testbox.yml
|
|
job: hydrate
|
|
ref: main
|
|
idleTimeout: 90m
|
|
debug: true
|
|
daytona:
|
|
apiUrl: https://daytona.example.test/api
|
|
snapshot: crabbox-ready
|
|
target: us
|
|
user: daytona
|
|
workRoot: /home/daytona/crabbox
|
|
sshGatewayHost: ssh.daytona.example.test
|
|
sshAccessMinutes: 12
|
|
islo:
|
|
baseUrl: https://islo.example.test
|
|
image: docker.io/library/ubuntu:24.04
|
|
workdir: crabbox
|
|
gatewayProfile: default
|
|
snapshotName: snap-ready
|
|
vcpus: 4
|
|
memoryMB: 8192
|
|
diskGB: 40
|
|
static:
|
|
id: win-dev
|
|
name: windows-dev
|
|
host: win-dev.local
|
|
user: peter
|
|
port: "22"
|
|
workRoot: /home/peter/crabbox
|
|
results:
|
|
junit:
|
|
- junit.xml
|
|
cache:
|
|
pnpm: true
|
|
npm: false
|
|
docker: true
|
|
git: true
|
|
maxGB: 120
|
|
purgeOnRelease: true
|
|
ssh:
|
|
key: ~/.ssh/crabbox
|
|
fallbackPorts:
|
|
- "22"
|
|
- "2022"
|
|
`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Provider != "aws" {
|
|
t.Fatalf("Provider=%q want aws", cfg.Provider)
|
|
}
|
|
if cfg.TargetOS != targetWindows || cfg.WindowsMode != windowsModeWSL2 {
|
|
t.Fatalf("target config not loaded: target=%s windowsMode=%s", cfg.TargetOS, cfg.WindowsMode)
|
|
}
|
|
if cfg.ServerType != "m8i.large" {
|
|
t.Fatalf("ServerType=%q want m8i.large", cfg.ServerType)
|
|
}
|
|
if cfg.SSHUser != "Administrator" {
|
|
t.Fatalf("SSHUser=%q want Administrator", cfg.SSHUser)
|
|
}
|
|
if cfg.Coordinator != "https://crabbox.example.test" || cfg.CoordToken != "secret" || cfg.CoordAdminToken != "admin-secret" {
|
|
t.Fatalf("broker config not loaded: %#v", cfg)
|
|
}
|
|
if cfg.Access.ClientID != "access-client" || cfg.Access.ClientSecret != "access-secret" || cfg.Access.Token != "access-jwt" {
|
|
t.Fatalf("access config not loaded: %#v", cfg.Access)
|
|
}
|
|
if cfg.TTL.String() != "2h0m0s" || cfg.IdleTimeout.String() != "45m0s" {
|
|
t.Fatalf("lease config not loaded: ttl=%s idle=%s", cfg.TTL, cfg.IdleTimeout)
|
|
}
|
|
if cfg.AWSRootGB != 800 {
|
|
t.Fatalf("AWSRootGB=%d want 800", cfg.AWSRootGB)
|
|
}
|
|
if len(cfg.AWSSSHCIDRs) != 1 || cfg.AWSSSHCIDRs[0] != "198.51.100.7/32" {
|
|
t.Fatalf("AWSSSHCIDRs=%v", cfg.AWSSSHCIDRs)
|
|
}
|
|
if cfg.SSHKey != filepath.Join(home, ".ssh", "crabbox") {
|
|
t.Fatalf("SSHKey=%q", cfg.SSHKey)
|
|
}
|
|
if len(cfg.SSHFallbackPorts) != 2 || cfg.SSHFallbackPorts[0] != "22" || cfg.SSHFallbackPorts[1] != "2022" {
|
|
t.Fatalf("SSHFallbackPorts=%v", cfg.SSHFallbackPorts)
|
|
}
|
|
if !cfg.Sync.Checksum || cfg.Sync.GitSeed || cfg.Sync.BaseRef != "trunk" {
|
|
t.Fatalf("sync config not loaded: %#v", cfg.Sync)
|
|
}
|
|
if cfg.Sync.Timeout.String() != "30m0s" || cfg.Sync.WarnFiles != 100 || cfg.Sync.WarnBytes != 200 || cfg.Sync.FailFiles != 300 || cfg.Sync.FailBytes != 400 || !cfg.Sync.AllowLarge {
|
|
t.Fatalf("sync guardrails not loaded: %#v", cfg.Sync)
|
|
}
|
|
if len(cfg.Sync.Excludes) != 2 || cfg.Sync.Excludes[0] != ".artifacts" || cfg.Sync.Excludes[1] != "tmp" {
|
|
t.Fatalf("sync excludes not loaded: %#v", cfg.Sync.Excludes)
|
|
}
|
|
if len(cfg.EnvAllow) != 3 || cfg.EnvAllow[2] != "CUSTOM_*" {
|
|
t.Fatalf("env allow not loaded: %#v", cfg.EnvAllow)
|
|
}
|
|
if cfg.Capacity.Strategy != "most-available" || cfg.Capacity.Hints || len(cfg.Capacity.Regions) != 1 || cfg.Capacity.Regions[0] != "eu-west-1" {
|
|
t.Fatalf("capacity config not loaded: %#v", cfg.Capacity)
|
|
}
|
|
if cfg.Actions.Repo != "openclaw/crabbox" || cfg.Actions.Workflow != ".github/workflows/crabbox.yml" || cfg.Actions.Job != "hydrate" || cfg.Actions.Ref != "main" {
|
|
t.Fatalf("actions config not loaded: %#v", cfg.Actions)
|
|
}
|
|
if len(cfg.Actions.Fields) != 2 || cfg.Actions.Fields[0] != "crabbox_docker_cache=true" || cfg.Actions.Fields[1] != "crabbox_prepare_images=1" {
|
|
t.Fatalf("actions fields config not loaded: %#v", cfg.Actions.Fields)
|
|
}
|
|
if cfg.Actions.Ephemeral || len(cfg.Actions.RunnerLabels) != 2 || cfg.Actions.RunnerLabels[1] != "linux-large" {
|
|
t.Fatalf("actions runner config not loaded: %#v", cfg.Actions)
|
|
}
|
|
if cfg.Blacksmith.Org != "openclaw" || cfg.Blacksmith.Workflow != ".github/workflows/blacksmith-testbox.yml" || cfg.Blacksmith.Job != "hydrate" || cfg.Blacksmith.Ref != "main" || cfg.Blacksmith.IdleTimeout != 90*time.Minute || !cfg.Blacksmith.Debug {
|
|
t.Fatalf("blacksmith config not loaded: %#v", cfg.Blacksmith)
|
|
}
|
|
if cfg.Daytona.APIURL != "https://daytona.example.test/api" || cfg.Daytona.Snapshot != "crabbox-ready" || cfg.Daytona.Target != "us" || cfg.Daytona.User != "daytona" || cfg.Daytona.WorkRoot != "/home/daytona/crabbox" || cfg.Daytona.SSHGatewayHost != "ssh.daytona.example.test" || cfg.Daytona.SSHAccessMinutes != 12 {
|
|
t.Fatalf("daytona config not loaded: %#v", cfg.Daytona)
|
|
}
|
|
if cfg.Islo.BaseURL != "https://islo.example.test" || cfg.Islo.Image != "docker.io/library/ubuntu:24.04" || cfg.Islo.Workdir != "crabbox" || cfg.Islo.GatewayProfile != "default" || cfg.Islo.SnapshotName != "snap-ready" || cfg.Islo.VCPUs != 4 || cfg.Islo.MemoryMB != 8192 || cfg.Islo.DiskGB != 40 {
|
|
t.Fatalf("islo config not loaded: %#v", cfg.Islo)
|
|
}
|
|
if cfg.Static.Host != "win-dev.local" || cfg.Static.User != "peter" || cfg.Static.Port != "22" || cfg.WorkRoot != "/home/peter/crabbox" {
|
|
t.Fatalf("static config not loaded: static=%#v workRoot=%s", cfg.Static, cfg.WorkRoot)
|
|
}
|
|
if len(cfg.Results.JUnit) != 1 || cfg.Results.JUnit[0] != "junit.xml" {
|
|
t.Fatalf("results config not loaded: %#v", cfg.Results)
|
|
}
|
|
if !cfg.Cache.Pnpm || cfg.Cache.Npm || !cfg.Cache.Docker || !cfg.Cache.Git || cfg.Cache.MaxGB != 120 || !cfg.Cache.PurgeOnRelease {
|
|
t.Fatalf("cache config not loaded: %#v", cfg.Cache)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigTailscaleBlock(t *testing.T) {
|
|
clearConfigEnv(t)
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("CRABBOX_CONFIG", "")
|
|
path := userConfigPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(`provider: aws
|
|
network: public
|
|
tailscale:
|
|
enabled: true
|
|
network: tailscale
|
|
tags:
|
|
- tag:crabbox
|
|
- tag:ci
|
|
hostnameTemplate: cbx-{slug}
|
|
authKeyEnv: TEST_TS_AUTH_KEY
|
|
exitNode: mac-studio.tailnet.ts.net
|
|
exitNodeAllowLanAccess: true
|
|
`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !cfg.Tailscale.Enabled || cfg.Network != NetworkTailscale || cfg.Tailscale.HostnameTemplate != "cbx-{slug}" || cfg.Tailscale.AuthKeyEnv != "TEST_TS_AUTH_KEY" || cfg.Tailscale.ExitNode != "mac-studio.tailnet.ts.net" || !cfg.Tailscale.ExitNodeAllowLANAccess {
|
|
t.Fatalf("tailscale config not loaded: network=%s tailscale=%#v", cfg.Network, cfg.Tailscale)
|
|
}
|
|
if len(cfg.Tailscale.Tags) != 2 || cfg.Tailscale.Tags[1] != "tag:ci" {
|
|
t.Fatalf("tailscale tags not loaded: %#v", cfg.Tailscale.Tags)
|
|
}
|
|
}
|
|
|
|
func TestEnvOverridesConfig(t *testing.T) {
|
|
clearConfigEnv(t)
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("CRABBOX_CONFIG", "")
|
|
t.Setenv("CRABBOX_PROVIDER", "hetzner")
|
|
t.Setenv("CRABBOX_DEFAULT_CLASS", "fast")
|
|
t.Setenv("CRABBOX_TTL", "3h")
|
|
t.Setenv("CRABBOX_IDLE_TIMEOUT", "20m")
|
|
t.Setenv("CRABBOX_AWS_SSH_CIDRS", "198.51.100.7/32,203.0.113.8/32")
|
|
t.Setenv("CRABBOX_SSH_FALLBACK_PORTS", "none")
|
|
t.Setenv("CRABBOX_ACCESS_CLIENT_ID", "env-access-client")
|
|
t.Setenv("CRABBOX_ACCESS_CLIENT_SECRET", "env-access-secret")
|
|
t.Setenv("CRABBOX_ACCESS_TOKEN", "env-access-jwt")
|
|
t.Setenv("CRABBOX_COORDINATOR_ADMIN_TOKEN", "env-admin-secret")
|
|
t.Setenv("CRABBOX_NETWORK", "public")
|
|
t.Setenv("CRABBOX_TAILSCALE_TAGS", "tag:crabbox,tag:ci")
|
|
t.Setenv("CRABBOX_TAILSCALE_HOSTNAME_TEMPLATE", "lease-{id}")
|
|
t.Setenv("CRABBOX_TAILSCALE_AUTH_KEY", "tskey-secret")
|
|
t.Setenv("CRABBOX_TAILSCALE_EXIT_NODE", "mac-studio.tailnet.ts.net")
|
|
t.Setenv("CRABBOX_TAILSCALE_EXIT_NODE_ALLOW_LAN_ACCESS", "1")
|
|
t.Setenv("CRABBOX_TARGET", "macos")
|
|
t.Setenv("CRABBOX_STATIC_HOST", "mac.local")
|
|
t.Setenv("DAYTONA_API_KEY", "daytona-api-file")
|
|
t.Setenv("CRABBOX_DAYTONA_API_KEY", "daytona-api-env")
|
|
t.Setenv("DAYTONA_API_URL", "https://daytona-file.example/api")
|
|
t.Setenv("CRABBOX_DAYTONA_API_URL", "https://daytona-env.example/api")
|
|
t.Setenv("DAYTONA_SNAPSHOT", "snapshot-file")
|
|
t.Setenv("CRABBOX_DAYTONA_SNAPSHOT", "snapshot-env")
|
|
t.Setenv("DAYTONA_TARGET", "target-file")
|
|
t.Setenv("CRABBOX_DAYTONA_TARGET", "target-env")
|
|
t.Setenv("CRABBOX_DAYTONA_USER", "daytona-env-user")
|
|
t.Setenv("CRABBOX_DAYTONA_WORK_ROOT", "/home/daytona/env")
|
|
t.Setenv("CRABBOX_DAYTONA_SSH_GATEWAY_HOST", "ssh.env.example")
|
|
t.Setenv("CRABBOX_DAYTONA_SSH_ACCESS_MINUTES", "44")
|
|
t.Setenv("ISLO_API_KEY", "islo-api-file")
|
|
t.Setenv("CRABBOX_ISLO_API_KEY", "islo-api-env")
|
|
t.Setenv("ISLO_BASE_URL", "https://islo-file.example")
|
|
t.Setenv("CRABBOX_ISLO_BASE_URL", "https://islo-env.example")
|
|
t.Setenv("CRABBOX_ISLO_IMAGE", "ubuntu:env")
|
|
t.Setenv("CRABBOX_ISLO_WORKDIR", "env-workdir")
|
|
t.Setenv("CRABBOX_ISLO_GATEWAY_PROFILE", "env-gateway")
|
|
t.Setenv("CRABBOX_ISLO_SNAPSHOT_NAME", "env-snapshot")
|
|
t.Setenv("CRABBOX_ISLO_VCPUS", "8")
|
|
t.Setenv("CRABBOX_ISLO_MEMORY_MB", "16384")
|
|
t.Setenv("CRABBOX_ISLO_DISK_GB", "80")
|
|
path := userConfigPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("provider: aws\nclass: beast\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Provider != "hetzner" || cfg.Class != "fast" || cfg.ServerType != "ccx43" || cfg.TTL.String() != "3h0m0s" || cfg.IdleTimeout.String() != "20m0s" {
|
|
t.Fatalf("unexpected config: provider=%s class=%s type=%s ttl=%s idle=%s", cfg.Provider, cfg.Class, cfg.ServerType, cfg.TTL, cfg.IdleTimeout)
|
|
}
|
|
if len(cfg.AWSSSHCIDRs) != 2 || cfg.AWSSSHCIDRs[0] != "198.51.100.7/32" || cfg.AWSSSHCIDRs[1] != "203.0.113.8/32" {
|
|
t.Fatalf("AWSSSHCIDRs=%v", cfg.AWSSSHCIDRs)
|
|
}
|
|
if len(cfg.SSHFallbackPorts) != 0 {
|
|
t.Fatalf("SSHFallbackPorts=%v want disabled fallback", cfg.SSHFallbackPorts)
|
|
}
|
|
if cfg.Access.ClientID != "env-access-client" || cfg.Access.ClientSecret != "env-access-secret" || cfg.Access.Token != "env-access-jwt" {
|
|
t.Fatalf("unexpected access config: %#v", cfg.Access)
|
|
}
|
|
if cfg.CoordAdminToken != "env-admin-secret" {
|
|
t.Fatalf("unexpected admin token state: %q", cfg.CoordAdminToken)
|
|
}
|
|
if cfg.TargetOS != targetMacOS || cfg.Static.Host != "mac.local" {
|
|
t.Fatalf("unexpected target env: target=%s static=%#v", cfg.TargetOS, cfg.Static)
|
|
}
|
|
if cfg.Network != NetworkPublic || cfg.Tailscale.AuthKey != "tskey-secret" || cfg.Tailscale.HostnameTemplate != "lease-{id}" || cfg.Tailscale.ExitNode != "mac-studio.tailnet.ts.net" || !cfg.Tailscale.ExitNodeAllowLANAccess {
|
|
t.Fatalf("unexpected tailscale env: network=%s tailscale=%#v", cfg.Network, cfg.Tailscale)
|
|
}
|
|
if len(cfg.Tailscale.Tags) != 2 || cfg.Tailscale.Tags[1] != "tag:ci" {
|
|
t.Fatalf("unexpected tailscale tags: %#v", cfg.Tailscale.Tags)
|
|
}
|
|
if cfg.Daytona.APIKey != "daytona-api-env" || cfg.Daytona.APIURL != "https://daytona-env.example/api" || cfg.Daytona.Snapshot != "snapshot-env" || cfg.Daytona.Target != "target-env" || cfg.Daytona.User != "daytona-env-user" || cfg.Daytona.WorkRoot != "/home/daytona/env" || cfg.Daytona.SSHGatewayHost != "ssh.env.example" || cfg.Daytona.SSHAccessMinutes != 44 {
|
|
t.Fatalf("unexpected daytona env: %#v", cfg.Daytona)
|
|
}
|
|
if cfg.Islo.APIKey != "islo-api-env" || cfg.Islo.BaseURL != "https://islo-env.example" || cfg.Islo.Image != "ubuntu:env" || cfg.Islo.Workdir != "env-workdir" || cfg.Islo.GatewayProfile != "env-gateway" || cfg.Islo.SnapshotName != "env-snapshot" || cfg.Islo.VCPUs != 8 || cfg.Islo.MemoryMB != 16384 || cfg.Islo.DiskGB != 80 {
|
|
t.Fatalf("unexpected islo env: %#v", cfg.Islo)
|
|
}
|
|
}
|
|
|
|
func TestTailscaleEnvOverrides(t *testing.T) {
|
|
clearConfigEnv(t)
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("CRABBOX_CONFIG", "")
|
|
t.Setenv("CRABBOX_PROVIDER", "hetzner")
|
|
t.Setenv("CRABBOX_NETWORK", "tailscale")
|
|
t.Setenv("CRABBOX_TAILSCALE", "1")
|
|
t.Setenv("CRABBOX_TAILSCALE_TAGS", "tag:crabbox,tag:ci")
|
|
t.Setenv("CRABBOX_TAILSCALE_HOSTNAME_TEMPLATE", "lease-{slug}")
|
|
t.Setenv("CRABBOX_TAILSCALE_AUTH_KEY", "tskey-secret")
|
|
t.Setenv("CRABBOX_TAILSCALE_EXIT_NODE", "100.100.100.100")
|
|
t.Setenv("CRABBOX_TAILSCALE_EXIT_NODE_ALLOW_LAN_ACCESS", "true")
|
|
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Network != NetworkTailscale || !cfg.Tailscale.Enabled || cfg.Tailscale.AuthKey != "tskey-secret" || cfg.Tailscale.HostnameTemplate != "lease-{slug}" || cfg.Tailscale.ExitNode != "100.100.100.100" || !cfg.Tailscale.ExitNodeAllowLANAccess {
|
|
t.Fatalf("unexpected tailscale env: network=%s tailscale=%#v", cfg.Network, cfg.Tailscale)
|
|
}
|
|
if len(cfg.Tailscale.Tags) != 2 || cfg.Tailscale.Tags[1] != "tag:ci" {
|
|
t.Fatalf("unexpected tailscale tags: %#v", cfg.Tailscale.Tags)
|
|
}
|
|
}
|
|
|
|
func TestInvalidNetworkConfigFails(t *testing.T) {
|
|
clearConfigEnv(t)
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("CRABBOX_CONFIG", "")
|
|
path := userConfigPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("network: private\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := loadConfig(); err == nil {
|
|
t.Fatal("expected invalid network config to fail")
|
|
}
|
|
}
|
|
|
|
func TestInvalidNetworkEnvFails(t *testing.T) {
|
|
clearConfigEnv(t)
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("CRABBOX_CONFIG", "")
|
|
t.Setenv("CRABBOX_NETWORK", "tailnet")
|
|
|
|
if _, err := loadConfig(); err == nil {
|
|
t.Fatal("expected invalid CRABBOX_NETWORK to fail")
|
|
}
|
|
}
|
|
|
|
func TestAccessAuthState(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
access AccessConfig
|
|
want string
|
|
}{
|
|
"missing": {
|
|
want: "missing",
|
|
},
|
|
"incomplete": {
|
|
access: AccessConfig{ClientID: "client"},
|
|
want: "incomplete",
|
|
},
|
|
"service token": {
|
|
access: AccessConfig{ClientID: "client", ClientSecret: "secret"},
|
|
want: "service-token",
|
|
},
|
|
"token": {
|
|
access: AccessConfig{Token: "jwt"},
|
|
want: "token",
|
|
},
|
|
"service token plus token": {
|
|
access: AccessConfig{ClientID: "client", ClientSecret: "secret", Token: "jwt"},
|
|
want: "service-token+token",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if got := accessAuthState(tc.access); got != tc.want {
|
|
t.Fatalf("accessAuthState()=%q want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepoConfigIsYamlOnly(t *testing.T) {
|
|
clearConfigEnv(t)
|
|
dir := t.TempDir()
|
|
oldwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chdir(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chdir(oldwd) })
|
|
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("CRABBOX_CONFIG", "")
|
|
t.Setenv("CRABBOX_PROVIDER", "")
|
|
t.Setenv("CRABBOX_DEFAULT_CLASS", "")
|
|
if err := os.WriteFile(".crabbox.json", []byte(`{"profile":"json-profile","provider":"aws"}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(".crabbox.yaml", []byte("profile: yaml-profile\nprovider: aws\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Profile != "yaml-profile" || cfg.Provider != "aws" {
|
|
t.Fatalf("unexpected config: profile=%s provider=%s", cfg.Profile, cfg.Provider)
|
|
}
|
|
}
|
|
|
|
func TestConfigHelperBranches(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("CRABBOX_CONFIG", filepath.Join(t.TempDir(), "explicit.yaml"))
|
|
|
|
if got := configPaths(); len(got) != 1 || got[0] != os.Getenv("CRABBOX_CONFIG") {
|
|
t.Fatalf("configPaths=%v", got)
|
|
}
|
|
if got := writableConfigPath(); got != os.Getenv("CRABBOX_CONFIG") {
|
|
t.Fatalf("writableConfigPath=%q", got)
|
|
}
|
|
|
|
cfgPath, err := writeUserFileConfig(fileConfig{Profile: "written", Provider: "aws"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfgPath != os.Getenv("CRABBOX_CONFIG") {
|
|
t.Fatalf("write path=%q", cfgPath)
|
|
}
|
|
file, err := readFileConfig(cfgPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if file.Profile != "written" || file.Provider != "aws" {
|
|
t.Fatalf("file config=%#v", file)
|
|
}
|
|
info, err := os.Stat(cfgPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := info.Mode().Perm(); got != 0o600 {
|
|
t.Fatalf("config mode=%04o want 0600", got)
|
|
}
|
|
|
|
if err := os.Chmod(cfgPath, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := writeUserFileConfig(fileConfig{Profile: "rewritten"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err = os.Stat(cfgPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := info.Mode().Perm(); got != 0o600 {
|
|
t.Fatalf("rewritten config mode=%04o want 0600", got)
|
|
}
|
|
if err := os.Chmod(cfgPath, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := configFilePermissionProblem(cfgPath); got == "" {
|
|
t.Fatal("expected config permission problem")
|
|
}
|
|
|
|
empty, err := readFileConfig(filepath.Join(t.TempDir(), "missing.yaml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if empty.Profile != "" {
|
|
t.Fatalf("missing file config=%#v", empty)
|
|
}
|
|
|
|
if got := expandUserPath("~"); got != home {
|
|
t.Fatalf("expand ~= %q want %q", got, home)
|
|
}
|
|
if got := expandUserPath("~/bin"); got != filepath.Join(home, "bin") {
|
|
t.Fatalf("expand ~/bin=%q", got)
|
|
}
|
|
if got := expandUserPath("/tmp/x"); got != "/tmp/x" {
|
|
t.Fatalf("absolute path changed to %q", got)
|
|
}
|
|
|
|
duration := 10 * time.Minute
|
|
applyLeaseDuration(&duration, "")
|
|
applyLeaseDuration(&duration, "bad")
|
|
applyLeaseDuration(&duration, "0s")
|
|
if duration != 10*time.Minute {
|
|
t.Fatalf("invalid durations changed value to %s", duration)
|
|
}
|
|
applyLeaseDuration(&duration, "15m")
|
|
if duration != 15*time.Minute {
|
|
t.Fatalf("duration=%s", duration)
|
|
}
|
|
}
|
|
|
|
func TestEnvHelperBranches(t *testing.T) {
|
|
t.Setenv("CRABBOX_INT", "42")
|
|
t.Setenv("CRABBOX_BAD_INT", "oops")
|
|
if got := getenvInt("CRABBOX_INT", 7); got != 42 {
|
|
t.Fatalf("int=%d", got)
|
|
}
|
|
if got := getenvInt("CRABBOX_BAD_INT", 7); got != 7 {
|
|
t.Fatalf("bad int fallback=%d", got)
|
|
}
|
|
if got := getenvInt("CRABBOX_MISSING_INT", 7); got != 7 {
|
|
t.Fatalf("missing int fallback=%d", got)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
value string
|
|
want bool
|
|
ok bool
|
|
}{
|
|
{"CRABBOX_BOOL_TRUE", "yes", true, true},
|
|
{"CRABBOX_BOOL_FALSE", "off", false, true},
|
|
{"CRABBOX_BOOL_BAD", "maybe", false, false},
|
|
{"CRABBOX_BOOL_EMPTY", "", false, false},
|
|
} {
|
|
if tc.value != "" {
|
|
t.Setenv(tc.name, tc.value)
|
|
}
|
|
got, ok := getenvBool(tc.name)
|
|
if got != tc.want || ok != tc.ok {
|
|
t.Fatalf("getenvBool(%s)=%v,%v want %v,%v", tc.name, got, ok, tc.want, tc.ok)
|
|
}
|
|
}
|
|
|
|
list := splitCommaList(" CI, ,NODE_OPTIONS,CUSTOM_* ")
|
|
if len(list) != 3 || list[0] != "CI" || list[2] != "CUSTOM_*" {
|
|
t.Fatalf("splitCommaList=%v", list)
|
|
}
|
|
}
|