crabbox/internal/cli/config.go
Jonathan Moss 00725544c7
feat(azure): support linux and native windows leases
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>
2026-05-08 08:23:38 +01:00

1356 lines
42 KiB
Go

package cli
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
Profile string
Provider string
TargetOS string
WindowsMode string
Desktop bool
Browser bool
Code bool
Network NetworkMode
Class string
ServerType string
ServerTypeExplicit bool
Coordinator string
CoordToken string
CoordAdminToken string
Access AccessConfig
Location string
Image string
AWSRegion string
AWSAMI string
AWSSGID string
AWSSubnetID string
AWSProfile string
AWSRootGB int32
AWSSSHCIDRs []string
AWSMacHostID string
AzureSubscription string
AzureTenant string
AzureClientID string
AzureLocation string
AzureResourceGroup string
AzureImage string
AzureVNet string
AzureSubnet string
AzureNSG string
AzureSSHCIDRs []string
SSHUser string
SSHKey string
SSHPort string
SSHFallbackPorts []string
ProviderKey string
WorkRoot string
TTL time.Duration
IdleTimeout time.Duration
Sync SyncConfig
EnvAllow []string
Capacity CapacityConfig
Actions ActionsConfig
Blacksmith BlacksmithConfig
Daytona DaytonaConfig
Islo IsloConfig
Tailscale TailscaleConfig
Static StaticConfig
Results ResultsConfig
Cache CacheConfig
}
type SyncConfig struct {
Excludes []string
Delete bool
Checksum bool
GitSeed bool
Fingerprint bool
BaseRef string
Timeout time.Duration
WarnFiles int
WarnBytes int64
FailFiles int
FailBytes int64
AllowLarge bool
}
type CapacityConfig struct {
Market string
Strategy string
Fallback string
Regions []string
AvailabilityZones []string
Hints bool
}
type ActionsConfig struct {
Repo string
Workflow string
Job string
Ref string
Fields []string
RunnerLabels []string
RunnerVersion string
Ephemeral bool
}
type BlacksmithConfig struct {
Org string
Workflow string
Job string
Ref string
IdleTimeout time.Duration
Debug bool
}
type DaytonaConfig struct {
APIKey string
JWTToken string
OrganizationID string
APIURL string
Snapshot string
Target string
User string
WorkRoot string
SSHGatewayHost string
SSHAccessMinutes int
}
type IsloConfig struct {
APIKey string
BaseURL string
Image string
Workdir string
GatewayProfile string
SnapshotName string
VCPUs int
MemoryMB int
DiskGB int
}
type StaticConfig struct {
ID string
Name string
Host string
User string
Port string
WorkRoot string
}
type ResultsConfig struct {
JUnit []string
}
type CacheConfig struct {
Pnpm bool
Npm bool
Docker bool
Git bool
MaxGB int
PurgeOnRelease bool
}
type AccessConfig struct {
ClientID string
ClientSecret string
Token string
}
func defaultConfig() Config {
cfg, err := loadConfig()
if err != nil {
return baseConfig()
}
return cfg
}
func loadConfig() (Config, error) {
cfg := baseConfig()
for _, path := range configPaths() {
if err := applyConfigFile(&cfg, path); err != nil {
return Config{}, err
}
}
applyEnv(&cfg)
normalizeTargetConfig(&cfg)
if err := validateTargetConfig(cfg); err != nil {
return Config{}, err
}
if err := validateNetworkConfig(cfg); err != nil {
return Config{}, err
}
if cfg.ServerType == "" {
cfg.ServerType = serverTypeForConfig(cfg)
}
return cfg, nil
}
func baseConfig() Config {
home, _ := os.UserHomeDir()
sshKey := ""
if home != "" {
sshKey = filepath.Join(home, ".ssh", "id_ed25519")
}
class := "beast"
provider := "hetzner"
return Config{
Profile: "default",
Provider: provider,
TargetOS: "linux",
WindowsMode: "normal",
Network: NetworkAuto,
Class: class,
ServerType: "",
Location: "fsn1",
Image: "ubuntu-24.04",
AWSRegion: "eu-west-1",
AWSRootGB: 400,
AzureLocation: "eastus",
AzureResourceGroup: "crabbox-leases",
AzureImage: defaultAzureLinuxImage,
AzureVNet: "crabbox-vnet",
AzureSubnet: "crabbox-subnet",
AzureNSG: "crabbox-nsg",
SSHUser: "crabbox",
SSHKey: sshKey,
SSHPort: "2222",
SSHFallbackPorts: []string{"22"},
ProviderKey: "crabbox-steipete",
WorkRoot: defaultPOSIXWorkRoot,
TTL: 90 * time.Minute,
IdleTimeout: 30 * time.Minute,
Sync: SyncConfig{
Delete: true,
Checksum: false,
GitSeed: true,
Fingerprint: true,
Timeout: 15 * time.Minute,
WarnFiles: 50_000,
WarnBytes: 5 * 1024 * 1024 * 1024,
FailFiles: 150_000,
FailBytes: 20 * 1024 * 1024 * 1024,
},
EnvAllow: []string{"CI", "NODE_OPTIONS"},
Capacity: CapacityConfig{
Market: "spot",
Strategy: "most-available",
Fallback: "on-demand-after-120s",
Hints: true,
},
Actions: ActionsConfig{
RunnerVersion: "latest",
Ephemeral: true,
},
Daytona: DaytonaConfig{
APIURL: "https://app.daytona.io/api",
User: "daytona",
WorkRoot: "/home/daytona/crabbox",
SSHGatewayHost: "ssh.app.daytona.io",
SSHAccessMinutes: 30,
},
Islo: IsloConfig{
BaseURL: "https://api.islo.dev",
Image: "docker.io/library/ubuntu:24.04",
Workdir: "crabbox",
VCPUs: 2,
MemoryMB: 4096,
DiskGB: 20,
},
Tailscale: TailscaleConfig{
Tags: []string{"tag:crabbox"},
HostnameTemplate: "crabbox-{slug}",
AuthKeyEnv: "CRABBOX_TAILSCALE_AUTH_KEY",
},
Cache: CacheConfig{
Pnpm: true,
Npm: true,
Docker: true,
Git: true,
MaxGB: 80,
},
}
}
type fileConfig struct {
Profile string `yaml:"profile,omitempty"`
Provider string `yaml:"provider,omitempty"`
Target string `yaml:"target,omitempty"`
TargetOS string `yaml:"targetOS,omitempty"`
Windows *fileWindowsConfig `yaml:"windows,omitempty"`
Desktop *bool `yaml:"desktop,omitempty"`
Browser *bool `yaml:"browser,omitempty"`
Code *bool `yaml:"code,omitempty"`
Network string `yaml:"network,omitempty"`
Class string `yaml:"class,omitempty"`
ServerType string `yaml:"serverType,omitempty"`
Coordinator string `yaml:"coordinator,omitempty"`
CoordinatorToken string `yaml:"coordinatorToken,omitempty"`
Broker *fileBrokerConfig `yaml:"broker,omitempty"`
Hetzner *fileHetznerConfig `yaml:"hetzner,omitempty"`
AWS *fileAWSConfig `yaml:"aws,omitempty"`
Azure *fileAzureConfig `yaml:"azure,omitempty"`
SSH *fileSSHConfig `yaml:"ssh,omitempty"`
Sync *fileSyncConfig `yaml:"sync,omitempty"`
Env *fileEnvConfig `yaml:"env,omitempty"`
Capacity *fileCapacityConfig `yaml:"capacity,omitempty"`
Actions *fileActionsConfig `yaml:"actions,omitempty"`
Blacksmith *fileBlacksmithConfig `yaml:"blacksmith,omitempty"`
Daytona *fileDaytonaConfig `yaml:"daytona,omitempty"`
Islo *fileIsloConfig `yaml:"islo,omitempty"`
Tailscale *fileTailscaleConfig `yaml:"tailscale,omitempty"`
Static *fileStaticConfig `yaml:"static,omitempty"`
Results *fileResultsConfig `yaml:"results,omitempty"`
Cache *fileCacheConfig `yaml:"cache,omitempty"`
Lease *fileLeaseConfig `yaml:"lease,omitempty"`
TTL string `yaml:"ttl,omitempty"`
IdleTimeout string `yaml:"idleTimeout,omitempty"`
WorkRoot string `yaml:"workRoot,omitempty"`
}
type fileWindowsConfig struct {
Mode string `yaml:"mode,omitempty"`
}
type fileBrokerConfig struct {
URL string `yaml:"url,omitempty"`
Token string `yaml:"token,omitempty"`
AdminToken string `yaml:"adminToken,omitempty"`
Provider string `yaml:"provider,omitempty"`
Access *fileAccessConfig `yaml:"access,omitempty"`
}
type fileAccessConfig struct {
ClientID string `yaml:"clientId,omitempty"`
ClientSecret string `yaml:"clientSecret,omitempty"`
Token string `yaml:"token,omitempty"`
}
type fileHetznerConfig struct {
Location string `yaml:"location,omitempty"`
Image string `yaml:"image,omitempty"`
SSHKey string `yaml:"sshKey,omitempty"`
}
type fileAWSConfig struct {
Region string `yaml:"region,omitempty"`
AMI string `yaml:"ami,omitempty"`
SecurityGroupID string `yaml:"securityGroupId,omitempty"`
SubnetID string `yaml:"subnetId,omitempty"`
InstanceProfile string `yaml:"instanceProfile,omitempty"`
RootGB int32 `yaml:"rootGB,omitempty"`
SSHCIDRs []string `yaml:"sshCIDRs,omitempty"`
MacHostID string `yaml:"macHostId,omitempty"`
}
type fileAzureConfig struct {
SubscriptionID string `yaml:"subscriptionId,omitempty"`
TenantID string `yaml:"tenantId,omitempty"`
ClientID string `yaml:"clientId,omitempty"`
Location string `yaml:"location,omitempty"`
ResourceGroup string `yaml:"resourceGroup,omitempty"`
Image string `yaml:"image,omitempty"`
VNet string `yaml:"vnet,omitempty"`
Subnet string `yaml:"subnet,omitempty"`
NSG string `yaml:"nsg,omitempty"`
SSHCIDRs []string `yaml:"sshCIDRs,omitempty"`
}
type fileSSHConfig struct {
User string `yaml:"user,omitempty"`
Key string `yaml:"key,omitempty"`
Port string `yaml:"port,omitempty"`
FallbackPorts *[]string `yaml:"fallbackPorts,omitempty"`
}
type fileSyncConfig struct {
Exclude []string `yaml:"exclude,omitempty"`
Excludes []string `yaml:"excludes,omitempty"`
Delete *bool `yaml:"delete,omitempty"`
Checksum *bool `yaml:"checksum,omitempty"`
GitSeed *bool `yaml:"gitSeed,omitempty"`
Fingerprint *bool `yaml:"fingerprint,omitempty"`
BaseRef string `yaml:"baseRef,omitempty"`
Timeout string `yaml:"timeout,omitempty"`
WarnFiles int `yaml:"warnFiles,omitempty"`
WarnBytes int64 `yaml:"warnBytes,omitempty"`
FailFiles int `yaml:"failFiles,omitempty"`
FailBytes int64 `yaml:"failBytes,omitempty"`
AllowLarge *bool `yaml:"allowLarge,omitempty"`
}
type fileEnvConfig struct {
Allow []string `yaml:"allow,omitempty"`
}
type fileCapacityConfig struct {
Market string `yaml:"market,omitempty"`
Strategy string `yaml:"strategy,omitempty"`
Fallback string `yaml:"fallback,omitempty"`
Regions []string `yaml:"regions,omitempty"`
AvailabilityZones []string `yaml:"availabilityZones,omitempty"`
Hints *bool `yaml:"hints,omitempty"`
}
type fileActionsConfig struct {
Repo string `yaml:"repo,omitempty"`
Workflow string `yaml:"workflow,omitempty"`
Job string `yaml:"job,omitempty"`
Ref string `yaml:"ref,omitempty"`
Fields []string `yaml:"fields,omitempty"`
RunnerLabels []string `yaml:"runnerLabels,omitempty"`
RunnerVersion string `yaml:"runnerVersion,omitempty"`
Ephemeral *bool `yaml:"ephemeral,omitempty"`
}
type fileBlacksmithConfig struct {
Org string `yaml:"org,omitempty"`
Workflow string `yaml:"workflow,omitempty"`
Job string `yaml:"job,omitempty"`
Ref string `yaml:"ref,omitempty"`
IdleTimeout string `yaml:"idleTimeout,omitempty"`
Debug *bool `yaml:"debug,omitempty"`
}
type fileDaytonaConfig struct {
APIURL string `yaml:"apiUrl,omitempty"`
Snapshot string `yaml:"snapshot,omitempty"`
Target string `yaml:"target,omitempty"`
User string `yaml:"user,omitempty"`
WorkRoot string `yaml:"workRoot,omitempty"`
SSHGatewayHost string `yaml:"sshGatewayHost,omitempty"`
SSHAccessMinutes int `yaml:"sshAccessMinutes,omitempty"`
}
type fileIsloConfig struct {
BaseURL string `yaml:"baseUrl,omitempty"`
Image string `yaml:"image,omitempty"`
Workdir string `yaml:"workdir,omitempty"`
GatewayProfile string `yaml:"gatewayProfile,omitempty"`
SnapshotName string `yaml:"snapshotName,omitempty"`
VCPUs int `yaml:"vcpus,omitempty"`
MemoryMB int `yaml:"memoryMB,omitempty"`
DiskGB int `yaml:"diskGB,omitempty"`
}
type fileTailscaleConfig struct {
Enabled *bool `yaml:"enabled,omitempty"`
Network string `yaml:"network,omitempty"`
Tags []string `yaml:"tags,omitempty"`
HostnameTemplate string `yaml:"hostnameTemplate,omitempty"`
AuthKeyEnv string `yaml:"authKeyEnv,omitempty"`
ExitNode string `yaml:"exitNode,omitempty"`
ExitNodeAllowLANAccess *bool `yaml:"exitNodeAllowLanAccess,omitempty"`
}
type fileStaticConfig struct {
ID string `yaml:"id,omitempty"`
Name string `yaml:"name,omitempty"`
Host string `yaml:"host,omitempty"`
User string `yaml:"user,omitempty"`
Port string `yaml:"port,omitempty"`
WorkRoot string `yaml:"workRoot,omitempty"`
}
type fileResultsConfig struct {
JUnit []string `yaml:"junit,omitempty"`
}
type fileCacheConfig struct {
Pnpm *bool `yaml:"pnpm,omitempty"`
Npm *bool `yaml:"npm,omitempty"`
Docker *bool `yaml:"docker,omitempty"`
Git *bool `yaml:"git,omitempty"`
MaxGB int `yaml:"maxGB,omitempty"`
PurgeOnRelease *bool `yaml:"purgeOnRelease,omitempty"`
}
type fileLeaseConfig struct {
TTL string `yaml:"ttl,omitempty"`
IdleTimeout string `yaml:"idleTimeout,omitempty"`
}
func configPaths() []string {
if explicit := os.Getenv("CRABBOX_CONFIG"); explicit != "" {
return []string{explicit}
}
paths := make([]string, 0, 3)
if userPath := userConfigPath(); userPath != "" {
paths = append(paths, userPath)
}
for _, path := range []string{"crabbox.yaml", ".crabbox.yaml"} {
if _, err := os.Stat(path); err == nil {
paths = append(paths, path)
}
}
return paths
}
func userConfigPath() string {
dir, err := os.UserConfigDir()
if err != nil {
return ""
}
return filepath.Join(dir, "crabbox", "config.yaml")
}
func readFileConfig(path string) (fileConfig, error) {
var cfg fileConfig
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return cfg, nil
}
return cfg, exit(2, "read config %s: %v", path, err)
}
if len(data) == 0 {
return cfg, nil
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return cfg, exit(2, "parse config %s: %v", path, err)
}
return cfg, nil
}
func writeUserFileConfig(cfg fileConfig) (string, error) {
path := writableConfigPath()
if path == "" {
return "", exit(2, "user config directory is unavailable")
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return "", exit(2, "create config directory: %v", err)
}
data, err := yaml.Marshal(cfg)
if err != nil {
return "", err
}
if err := os.WriteFile(path, data, 0o600); err != nil {
return "", exit(2, "write config %s: %v", path, err)
}
if err := os.Chmod(path, 0o600); err != nil {
return "", exit(2, "secure config %s: %v", path, err)
}
return path, nil
}
func configFilePermissionProblem(path string) string {
if path == "" {
return ""
}
info, err := os.Stat(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return ""
}
return err.Error()
}
if info.Mode().Perm()&0o077 != 0 {
return fmt.Sprintf("permissions %04o want 0600", info.Mode().Perm())
}
return ""
}
func writableConfigPath() string {
if explicit := os.Getenv("CRABBOX_CONFIG"); explicit != "" {
return explicit
}
return userConfigPath()
}
func applyConfigFile(cfg *Config, path string) error {
file, err := readFileConfig(path)
if err != nil {
return err
}
applyFileConfig(cfg, file)
return nil
}
func applyFileConfig(cfg *Config, file fileConfig) {
if file.Profile != "" {
cfg.Profile = file.Profile
}
if file.Provider != "" {
cfg.Provider = file.Provider
}
if file.Target != "" {
cfg.TargetOS = file.Target
}
if file.TargetOS != "" {
cfg.TargetOS = file.TargetOS
}
if file.Windows != nil && file.Windows.Mode != "" {
cfg.WindowsMode = file.Windows.Mode
}
if file.Desktop != nil {
cfg.Desktop = *file.Desktop
}
if file.Browser != nil {
cfg.Browser = *file.Browser
}
if file.Code != nil {
cfg.Code = *file.Code
}
if file.Network != "" {
cfg.Network = NetworkMode(strings.ToLower(strings.TrimSpace(file.Network)))
}
if file.Class != "" {
cfg.Class = file.Class
}
if file.ServerType != "" {
cfg.ServerType = file.ServerType
}
if file.Coordinator != "" {
cfg.Coordinator = file.Coordinator
}
if file.CoordinatorToken != "" {
cfg.CoordToken = file.CoordinatorToken
}
if file.Broker != nil {
if file.Broker.URL != "" {
cfg.Coordinator = file.Broker.URL
}
if file.Broker.Token != "" {
cfg.CoordToken = file.Broker.Token
}
if file.Broker.AdminToken != "" {
cfg.CoordAdminToken = file.Broker.AdminToken
}
if file.Broker.Provider != "" {
cfg.Provider = file.Broker.Provider
}
if file.Broker.Access != nil {
if file.Broker.Access.ClientID != "" {
cfg.Access.ClientID = file.Broker.Access.ClientID
}
if file.Broker.Access.ClientSecret != "" {
cfg.Access.ClientSecret = file.Broker.Access.ClientSecret
}
if file.Broker.Access.Token != "" {
cfg.Access.Token = file.Broker.Access.Token
}
}
}
if file.Hetzner != nil {
if file.Hetzner.Location != "" {
cfg.Location = file.Hetzner.Location
}
if file.Hetzner.Image != "" {
cfg.Image = file.Hetzner.Image
}
if file.Hetzner.SSHKey != "" {
cfg.ProviderKey = file.Hetzner.SSHKey
}
}
if file.AWS != nil {
if file.AWS.Region != "" {
cfg.AWSRegion = file.AWS.Region
}
if file.AWS.AMI != "" {
cfg.AWSAMI = file.AWS.AMI
}
if file.AWS.SecurityGroupID != "" {
cfg.AWSSGID = file.AWS.SecurityGroupID
}
if file.AWS.SubnetID != "" {
cfg.AWSSubnetID = file.AWS.SubnetID
}
if file.AWS.InstanceProfile != "" {
cfg.AWSProfile = file.AWS.InstanceProfile
}
if file.AWS.RootGB > 0 {
cfg.AWSRootGB = file.AWS.RootGB
}
if len(file.AWS.SSHCIDRs) > 0 {
cfg.AWSSSHCIDRs = file.AWS.SSHCIDRs
}
if file.AWS.MacHostID != "" {
cfg.AWSMacHostID = file.AWS.MacHostID
}
}
if file.Azure != nil {
if file.Azure.SubscriptionID != "" {
cfg.AzureSubscription = file.Azure.SubscriptionID
}
if file.Azure.TenantID != "" {
cfg.AzureTenant = file.Azure.TenantID
}
if file.Azure.ClientID != "" {
cfg.AzureClientID = file.Azure.ClientID
}
if file.Azure.Location != "" {
cfg.AzureLocation = file.Azure.Location
}
if file.Azure.ResourceGroup != "" {
cfg.AzureResourceGroup = file.Azure.ResourceGroup
}
if file.Azure.Image != "" {
cfg.AzureImage = file.Azure.Image
}
if file.Azure.VNet != "" {
cfg.AzureVNet = file.Azure.VNet
}
if file.Azure.Subnet != "" {
cfg.AzureSubnet = file.Azure.Subnet
}
if file.Azure.NSG != "" {
cfg.AzureNSG = file.Azure.NSG
}
if len(file.Azure.SSHCIDRs) > 0 {
cfg.AzureSSHCIDRs = file.Azure.SSHCIDRs
}
}
if file.SSH != nil {
if file.SSH.User != "" {
cfg.SSHUser = file.SSH.User
}
if file.SSH.Key != "" {
cfg.SSHKey = expandUserPath(file.SSH.Key)
}
if file.SSH.Port != "" {
cfg.SSHPort = file.SSH.Port
}
if file.SSH.FallbackPorts != nil {
cfg.SSHFallbackPorts = normalizeList(*file.SSH.FallbackPorts)
}
}
if file.WorkRoot != "" {
cfg.WorkRoot = file.WorkRoot
}
applyLeaseDuration(&cfg.TTL, file.TTL)
applyLeaseDuration(&cfg.IdleTimeout, file.IdleTimeout)
if file.Lease != nil {
applyLeaseDuration(&cfg.TTL, file.Lease.TTL)
applyLeaseDuration(&cfg.IdleTimeout, file.Lease.IdleTimeout)
}
if file.Sync != nil {
cfg.Sync.Excludes = appendUniqueStrings(cfg.Sync.Excludes, file.Sync.Exclude...)
cfg.Sync.Excludes = appendUniqueStrings(cfg.Sync.Excludes, file.Sync.Excludes...)
if file.Sync.Delete != nil {
cfg.Sync.Delete = *file.Sync.Delete
}
if file.Sync.Checksum != nil {
cfg.Sync.Checksum = *file.Sync.Checksum
}
if file.Sync.GitSeed != nil {
cfg.Sync.GitSeed = *file.Sync.GitSeed
}
if file.Sync.Fingerprint != nil {
cfg.Sync.Fingerprint = *file.Sync.Fingerprint
}
if file.Sync.BaseRef != "" {
cfg.Sync.BaseRef = file.Sync.BaseRef
}
if file.Sync.Timeout != "" {
if timeout, err := time.ParseDuration(file.Sync.Timeout); err == nil {
cfg.Sync.Timeout = timeout
}
}
if file.Sync.WarnFiles > 0 {
cfg.Sync.WarnFiles = file.Sync.WarnFiles
}
if file.Sync.WarnBytes > 0 {
cfg.Sync.WarnBytes = file.Sync.WarnBytes
}
if file.Sync.FailFiles > 0 {
cfg.Sync.FailFiles = file.Sync.FailFiles
}
if file.Sync.FailBytes > 0 {
cfg.Sync.FailBytes = file.Sync.FailBytes
}
if file.Sync.AllowLarge != nil {
cfg.Sync.AllowLarge = *file.Sync.AllowLarge
}
}
if file.Env != nil && len(file.Env.Allow) > 0 {
cfg.EnvAllow = appendUniqueStrings(nil, file.Env.Allow...)
}
if file.Capacity != nil {
if file.Capacity.Market != "" {
cfg.Capacity.Market = file.Capacity.Market
}
if file.Capacity.Strategy != "" {
cfg.Capacity.Strategy = file.Capacity.Strategy
}
if file.Capacity.Fallback != "" {
cfg.Capacity.Fallback = file.Capacity.Fallback
}
if len(file.Capacity.Regions) > 0 {
cfg.Capacity.Regions = appendUniqueStrings(nil, file.Capacity.Regions...)
}
if len(file.Capacity.AvailabilityZones) > 0 {
cfg.Capacity.AvailabilityZones = appendUniqueStrings(nil, file.Capacity.AvailabilityZones...)
}
if file.Capacity.Hints != nil {
cfg.Capacity.Hints = *file.Capacity.Hints
}
}
if file.Actions != nil {
if file.Actions.Repo != "" {
cfg.Actions.Repo = file.Actions.Repo
}
if file.Actions.Workflow != "" {
cfg.Actions.Workflow = file.Actions.Workflow
}
if file.Actions.Job != "" {
cfg.Actions.Job = file.Actions.Job
}
if file.Actions.Ref != "" {
cfg.Actions.Ref = file.Actions.Ref
}
if len(file.Actions.Fields) > 0 {
cfg.Actions.Fields = appendUniqueStrings(nil, file.Actions.Fields...)
}
if len(file.Actions.RunnerLabels) > 0 {
cfg.Actions.RunnerLabels = appendUniqueStrings(nil, file.Actions.RunnerLabels...)
}
if file.Actions.RunnerVersion != "" {
cfg.Actions.RunnerVersion = file.Actions.RunnerVersion
}
if file.Actions.Ephemeral != nil {
cfg.Actions.Ephemeral = *file.Actions.Ephemeral
}
}
if file.Blacksmith != nil {
if file.Blacksmith.Org != "" {
cfg.Blacksmith.Org = file.Blacksmith.Org
}
if file.Blacksmith.Workflow != "" {
cfg.Blacksmith.Workflow = file.Blacksmith.Workflow
}
if file.Blacksmith.Job != "" {
cfg.Blacksmith.Job = file.Blacksmith.Job
}
if file.Blacksmith.Ref != "" {
cfg.Blacksmith.Ref = file.Blacksmith.Ref
}
applyLeaseDuration(&cfg.Blacksmith.IdleTimeout, file.Blacksmith.IdleTimeout)
if file.Blacksmith.Debug != nil {
cfg.Blacksmith.Debug = *file.Blacksmith.Debug
}
}
if file.Daytona != nil {
if file.Daytona.APIURL != "" {
cfg.Daytona.APIURL = file.Daytona.APIURL
}
if file.Daytona.Snapshot != "" {
cfg.Daytona.Snapshot = file.Daytona.Snapshot
}
if file.Daytona.Target != "" {
cfg.Daytona.Target = file.Daytona.Target
}
if file.Daytona.User != "" {
cfg.Daytona.User = file.Daytona.User
}
if file.Daytona.WorkRoot != "" {
cfg.Daytona.WorkRoot = file.Daytona.WorkRoot
}
if file.Daytona.SSHGatewayHost != "" {
cfg.Daytona.SSHGatewayHost = file.Daytona.SSHGatewayHost
}
if file.Daytona.SSHAccessMinutes > 0 {
cfg.Daytona.SSHAccessMinutes = file.Daytona.SSHAccessMinutes
}
}
if file.Islo != nil {
if file.Islo.BaseURL != "" {
cfg.Islo.BaseURL = file.Islo.BaseURL
}
if file.Islo.Image != "" {
cfg.Islo.Image = file.Islo.Image
}
if file.Islo.Workdir != "" {
cfg.Islo.Workdir = file.Islo.Workdir
}
if file.Islo.GatewayProfile != "" {
cfg.Islo.GatewayProfile = file.Islo.GatewayProfile
}
if file.Islo.SnapshotName != "" {
cfg.Islo.SnapshotName = file.Islo.SnapshotName
}
if file.Islo.VCPUs > 0 {
cfg.Islo.VCPUs = file.Islo.VCPUs
}
if file.Islo.MemoryMB > 0 {
cfg.Islo.MemoryMB = file.Islo.MemoryMB
}
if file.Islo.DiskGB > 0 {
cfg.Islo.DiskGB = file.Islo.DiskGB
}
}
if file.Tailscale != nil {
if file.Tailscale.Enabled != nil {
cfg.Tailscale.Enabled = *file.Tailscale.Enabled
}
if file.Tailscale.Network != "" {
cfg.Network = NetworkMode(strings.ToLower(strings.TrimSpace(file.Tailscale.Network)))
}
if len(file.Tailscale.Tags) > 0 {
cfg.Tailscale.Tags = normalizeTailscaleTags(file.Tailscale.Tags)
}
if file.Tailscale.HostnameTemplate != "" {
cfg.Tailscale.HostnameTemplate = file.Tailscale.HostnameTemplate
}
if file.Tailscale.AuthKeyEnv != "" {
cfg.Tailscale.AuthKeyEnv = file.Tailscale.AuthKeyEnv
}
if file.Tailscale.ExitNode != "" {
cfg.Tailscale.ExitNode = strings.TrimSpace(file.Tailscale.ExitNode)
}
if file.Tailscale.ExitNodeAllowLANAccess != nil {
cfg.Tailscale.ExitNodeAllowLANAccess = *file.Tailscale.ExitNodeAllowLANAccess
}
}
if file.Static != nil {
if file.Static.ID != "" {
cfg.Static.ID = file.Static.ID
}
if file.Static.Name != "" {
cfg.Static.Name = file.Static.Name
}
if file.Static.Host != "" {
cfg.Static.Host = file.Static.Host
}
if file.Static.User != "" {
cfg.Static.User = file.Static.User
}
if file.Static.Port != "" {
cfg.Static.Port = file.Static.Port
}
if file.Static.WorkRoot != "" {
cfg.Static.WorkRoot = file.Static.WorkRoot
}
}
if file.Results != nil && len(file.Results.JUnit) > 0 {
cfg.Results.JUnit = appendUniqueStrings(nil, file.Results.JUnit...)
}
if file.Cache != nil {
if file.Cache.Pnpm != nil {
cfg.Cache.Pnpm = *file.Cache.Pnpm
}
if file.Cache.Npm != nil {
cfg.Cache.Npm = *file.Cache.Npm
}
if file.Cache.Docker != nil {
cfg.Cache.Docker = *file.Cache.Docker
}
if file.Cache.Git != nil {
cfg.Cache.Git = *file.Cache.Git
}
if file.Cache.MaxGB > 0 {
cfg.Cache.MaxGB = file.Cache.MaxGB
}
if file.Cache.PurgeOnRelease != nil {
cfg.Cache.PurgeOnRelease = *file.Cache.PurgeOnRelease
}
}
}
func applyLeaseDuration(target *time.Duration, value string) {
if value == "" {
return
}
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
*target = parsed
}
}
func applyEnv(cfg *Config) {
cfg.Profile = getenv("CRABBOX_PROFILE", cfg.Profile)
cfg.Provider = getenv("CRABBOX_PROVIDER", cfg.Provider)
cfg.TargetOS = getenv("CRABBOX_TARGET", getenv("CRABBOX_TARGET_OS", cfg.TargetOS))
cfg.WindowsMode = getenv("CRABBOX_WINDOWS_MODE", cfg.WindowsMode)
if value, ok := getenvBool("CRABBOX_DESKTOP"); ok {
cfg.Desktop = value
}
if value, ok := getenvBool("CRABBOX_BROWSER"); ok {
cfg.Browser = value
}
if value, ok := getenvBool("CRABBOX_CODE"); ok {
cfg.Code = value
}
if network := os.Getenv("CRABBOX_NETWORK"); network != "" {
cfg.Network = NetworkMode(strings.ToLower(strings.TrimSpace(network)))
}
cfg.Class = getenv("CRABBOX_DEFAULT_CLASS", cfg.Class)
if os.Getenv("CRABBOX_SERVER_TYPE") != "" {
cfg.ServerTypeExplicit = true
}
cfg.ServerType = getenv("CRABBOX_SERVER_TYPE", cfg.ServerType)
cfg.Coordinator = getenv("CRABBOX_COORDINATOR", cfg.Coordinator)
cfg.CoordToken = getenv("CRABBOX_COORDINATOR_TOKEN", cfg.CoordToken)
cfg.CoordAdminToken = getenv("CRABBOX_COORDINATOR_ADMIN_TOKEN", getenv("CRABBOX_ADMIN_TOKEN", cfg.CoordAdminToken))
cfg.Access.ClientID = getenv("CRABBOX_ACCESS_CLIENT_ID", getenv("CF_ACCESS_CLIENT_ID", cfg.Access.ClientID))
cfg.Access.ClientSecret = getenv("CRABBOX_ACCESS_CLIENT_SECRET", getenv("CF_ACCESS_CLIENT_SECRET", cfg.Access.ClientSecret))
cfg.Access.Token = getenv("CRABBOX_ACCESS_TOKEN", getenv("CF_ACCESS_TOKEN", cfg.Access.Token))
cfg.Location = getenv("CRABBOX_HETZNER_LOCATION", cfg.Location)
cfg.Image = getenv("CRABBOX_HETZNER_IMAGE", cfg.Image)
cfg.AWSRegion = getenv("CRABBOX_AWS_REGION", getenv("AWS_REGION", cfg.AWSRegion))
cfg.AWSAMI = getenv("CRABBOX_AWS_AMI", cfg.AWSAMI)
cfg.AWSSGID = getenv("CRABBOX_AWS_SECURITY_GROUP_ID", cfg.AWSSGID)
cfg.AWSSubnetID = getenv("CRABBOX_AWS_SUBNET_ID", cfg.AWSSubnetID)
cfg.AWSProfile = getenv("CRABBOX_AWS_INSTANCE_PROFILE", cfg.AWSProfile)
cfg.AWSRootGB = int32(getenvInt("CRABBOX_AWS_ROOT_GB", int(cfg.AWSRootGB)))
cfg.AWSMacHostID = getenv("CRABBOX_AWS_MAC_HOST_ID", cfg.AWSMacHostID)
if cidrs := os.Getenv("CRABBOX_AWS_SSH_CIDRS"); cidrs != "" {
cfg.AWSSSHCIDRs = splitCommaList(cidrs)
}
cfg.AzureSubscription = getenv("CRABBOX_AZURE_SUBSCRIPTION_ID", getenv("AZURE_SUBSCRIPTION_ID", cfg.AzureSubscription))
cfg.AzureTenant = getenv("CRABBOX_AZURE_TENANT_ID", getenv("AZURE_TENANT_ID", cfg.AzureTenant))
cfg.AzureClientID = getenv("CRABBOX_AZURE_CLIENT_ID", getenv("AZURE_CLIENT_ID", cfg.AzureClientID))
cfg.AzureLocation = getenv("CRABBOX_AZURE_LOCATION", cfg.AzureLocation)
cfg.AzureResourceGroup = getenv("CRABBOX_AZURE_RESOURCE_GROUP", cfg.AzureResourceGroup)
cfg.AzureImage = getenv("CRABBOX_AZURE_IMAGE", cfg.AzureImage)
cfg.AzureVNet = getenv("CRABBOX_AZURE_VNET", cfg.AzureVNet)
cfg.AzureSubnet = getenv("CRABBOX_AZURE_SUBNET", cfg.AzureSubnet)
cfg.AzureNSG = getenv("CRABBOX_AZURE_NSG", cfg.AzureNSG)
if cidrs := os.Getenv("CRABBOX_AZURE_SSH_CIDRS"); cidrs != "" {
cfg.AzureSSHCIDRs = splitCommaList(cidrs)
}
cfg.SSHUser = getenv("CRABBOX_SSH_USER", cfg.SSHUser)
cfg.SSHKey = getenv("CRABBOX_SSH_KEY", cfg.SSHKey)
cfg.SSHPort = getenv("CRABBOX_SSH_PORT", cfg.SSHPort)
if ports, ok := getenvList("CRABBOX_SSH_FALLBACK_PORTS"); ok {
cfg.SSHFallbackPorts = ports
}
cfg.ProviderKey = getenv("CRABBOX_HETZNER_SSH_KEY", cfg.ProviderKey)
cfg.WorkRoot = getenv("CRABBOX_WORK_ROOT", cfg.WorkRoot)
if ttl := os.Getenv("CRABBOX_TTL"); ttl != "" {
applyLeaseDuration(&cfg.TTL, ttl)
}
if idleTimeout := os.Getenv("CRABBOX_IDLE_TIMEOUT"); idleTimeout != "" {
applyLeaseDuration(&cfg.IdleTimeout, idleTimeout)
}
cfg.Capacity.Market = getenv("CRABBOX_CAPACITY_MARKET", cfg.Capacity.Market)
cfg.Capacity.Strategy = getenv("CRABBOX_CAPACITY_STRATEGY", cfg.Capacity.Strategy)
cfg.Capacity.Fallback = getenv("CRABBOX_CAPACITY_FALLBACK", cfg.Capacity.Fallback)
if value, ok := getenvBool("CRABBOX_CAPACITY_HINTS"); ok {
cfg.Capacity.Hints = value
}
cfg.Actions.Workflow = getenv("CRABBOX_ACTIONS_WORKFLOW", cfg.Actions.Workflow)
cfg.Actions.Job = getenv("CRABBOX_ACTIONS_JOB", cfg.Actions.Job)
cfg.Actions.Ref = getenv("CRABBOX_ACTIONS_REF", cfg.Actions.Ref)
cfg.Actions.Repo = getenv("CRABBOX_ACTIONS_REPO", cfg.Actions.Repo)
cfg.Actions.RunnerVersion = getenv("CRABBOX_ACTIONS_RUNNER_VERSION", cfg.Actions.RunnerVersion)
cfg.Blacksmith.Org = getenv("CRABBOX_BLACKSMITH_ORG", cfg.Blacksmith.Org)
cfg.Blacksmith.Workflow = getenv("CRABBOX_BLACKSMITH_WORKFLOW", cfg.Blacksmith.Workflow)
cfg.Blacksmith.Job = getenv("CRABBOX_BLACKSMITH_JOB", cfg.Blacksmith.Job)
cfg.Blacksmith.Ref = getenv("CRABBOX_BLACKSMITH_REF", cfg.Blacksmith.Ref)
cfg.Daytona.APIKey = getenv("CRABBOX_DAYTONA_API_KEY", getenv("DAYTONA_API_KEY", cfg.Daytona.APIKey))
cfg.Daytona.JWTToken = getenv("CRABBOX_DAYTONA_JWT_TOKEN", getenv("DAYTONA_JWT_TOKEN", cfg.Daytona.JWTToken))
cfg.Daytona.OrganizationID = getenv("CRABBOX_DAYTONA_ORGANIZATION_ID", getenv("DAYTONA_ORGANIZATION_ID", cfg.Daytona.OrganizationID))
cfg.Daytona.APIURL = getenv("CRABBOX_DAYTONA_API_URL", getenv("DAYTONA_API_URL", cfg.Daytona.APIURL))
cfg.Daytona.Snapshot = getenv("CRABBOX_DAYTONA_SNAPSHOT", getenv("DAYTONA_SNAPSHOT", cfg.Daytona.Snapshot))
cfg.Daytona.Target = getenv("CRABBOX_DAYTONA_TARGET", getenv("DAYTONA_TARGET", cfg.Daytona.Target))
cfg.Daytona.User = getenv("CRABBOX_DAYTONA_USER", cfg.Daytona.User)
cfg.Daytona.WorkRoot = getenv("CRABBOX_DAYTONA_WORK_ROOT", cfg.Daytona.WorkRoot)
cfg.Daytona.SSHGatewayHost = getenv("CRABBOX_DAYTONA_SSH_GATEWAY_HOST", cfg.Daytona.SSHGatewayHost)
cfg.Daytona.SSHAccessMinutes = getenvInt("CRABBOX_DAYTONA_SSH_ACCESS_MINUTES", cfg.Daytona.SSHAccessMinutes)
cfg.Islo.APIKey = getenv("CRABBOX_ISLO_API_KEY", getenv("ISLO_API_KEY", cfg.Islo.APIKey))
cfg.Islo.BaseURL = getenv("CRABBOX_ISLO_BASE_URL", getenv("ISLO_BASE_URL", cfg.Islo.BaseURL))
cfg.Islo.Image = getenv("CRABBOX_ISLO_IMAGE", cfg.Islo.Image)
cfg.Islo.Workdir = getenv("CRABBOX_ISLO_WORKDIR", cfg.Islo.Workdir)
cfg.Islo.GatewayProfile = getenv("CRABBOX_ISLO_GATEWAY_PROFILE", cfg.Islo.GatewayProfile)
cfg.Islo.SnapshotName = getenv("CRABBOX_ISLO_SNAPSHOT_NAME", cfg.Islo.SnapshotName)
cfg.Islo.VCPUs = getenvInt("CRABBOX_ISLO_VCPUS", cfg.Islo.VCPUs)
cfg.Islo.MemoryMB = getenvInt("CRABBOX_ISLO_MEMORY_MB", cfg.Islo.MemoryMB)
cfg.Islo.DiskGB = getenvInt("CRABBOX_ISLO_DISK_GB", cfg.Islo.DiskGB)
if value, ok := getenvBool("CRABBOX_TAILSCALE"); ok {
cfg.Tailscale.Enabled = value
}
if tags := os.Getenv("CRABBOX_TAILSCALE_TAGS"); tags != "" {
cfg.Tailscale.Tags = normalizeTailscaleTags(splitCommaList(tags))
}
cfg.Tailscale.HostnameTemplate = getenv("CRABBOX_TAILSCALE_HOSTNAME_TEMPLATE", cfg.Tailscale.HostnameTemplate)
cfg.Tailscale.AuthKeyEnv = getenv("CRABBOX_TAILSCALE_AUTH_KEY_ENV", cfg.Tailscale.AuthKeyEnv)
cfg.Tailscale.ExitNode = getenv("CRABBOX_TAILSCALE_EXIT_NODE", cfg.Tailscale.ExitNode)
if value, ok := getenvBool("CRABBOX_TAILSCALE_EXIT_NODE_ALLOW_LAN_ACCESS"); ok {
cfg.Tailscale.ExitNodeAllowLANAccess = value
}
if cfg.Tailscale.AuthKeyEnv != "" {
cfg.Tailscale.AuthKey = getenv(cfg.Tailscale.AuthKeyEnv, "")
}
cfg.Static.ID = getenv("CRABBOX_STATIC_ID", cfg.Static.ID)
cfg.Static.Name = getenv("CRABBOX_STATIC_NAME", cfg.Static.Name)
cfg.Static.Host = getenv("CRABBOX_STATIC_HOST", cfg.Static.Host)
cfg.Static.User = getenv("CRABBOX_STATIC_USER", cfg.Static.User)
cfg.Static.Port = getenv("CRABBOX_STATIC_PORT", cfg.Static.Port)
cfg.Static.WorkRoot = getenv("CRABBOX_STATIC_WORK_ROOT", cfg.Static.WorkRoot)
if idleTimeout := os.Getenv("CRABBOX_BLACKSMITH_IDLE_TIMEOUT"); idleTimeout != "" {
applyLeaseDuration(&cfg.Blacksmith.IdleTimeout, idleTimeout)
}
if value, ok := getenvBool("CRABBOX_BLACKSMITH_DEBUG"); ok {
cfg.Blacksmith.Debug = value
}
if labels := os.Getenv("CRABBOX_ACTIONS_RUNNER_LABELS"); labels != "" {
cfg.Actions.RunnerLabels = splitCommaList(labels)
}
if value, ok := getenvBool("CRABBOX_ACTIONS_EPHEMERAL"); ok {
cfg.Actions.Ephemeral = value
}
if junit := os.Getenv("CRABBOX_RESULTS_JUNIT"); junit != "" {
cfg.Results.JUnit = splitCommaList(junit)
}
if value, ok := getenvBool("CRABBOX_CACHE_PNPM"); ok {
cfg.Cache.Pnpm = value
}
if value, ok := getenvBool("CRABBOX_CACHE_NPM"); ok {
cfg.Cache.Npm = value
}
if value, ok := getenvBool("CRABBOX_CACHE_DOCKER"); ok {
cfg.Cache.Docker = value
}
if value, ok := getenvBool("CRABBOX_CACHE_GIT"); ok {
cfg.Cache.Git = value
}
cfg.Cache.MaxGB = getenvInt("CRABBOX_CACHE_MAX_GB", cfg.Cache.MaxGB)
if value, ok := getenvBool("CRABBOX_CACHE_PURGE_ON_RELEASE"); ok {
cfg.Cache.PurgeOnRelease = value
}
if regions := os.Getenv("CRABBOX_CAPACITY_REGIONS"); regions != "" {
cfg.Capacity.Regions = splitCommaList(regions)
}
if zones := os.Getenv("CRABBOX_CAPACITY_AVAILABILITY_ZONES"); zones != "" {
cfg.Capacity.AvailabilityZones = splitCommaList(zones)
}
if value, ok := getenvBool("CRABBOX_SYNC_CHECKSUM"); ok {
cfg.Sync.Checksum = value
}
if value, ok := getenvBool("CRABBOX_SYNC_DELETE"); ok {
cfg.Sync.Delete = value
}
if value, ok := getenvBool("CRABBOX_SYNC_GIT_SEED"); ok {
cfg.Sync.GitSeed = value
}
if value, ok := getenvBool("CRABBOX_SYNC_FINGERPRINT"); ok {
cfg.Sync.Fingerprint = value
}
if timeout := os.Getenv("CRABBOX_SYNC_TIMEOUT"); timeout != "" {
if parsed, err := time.ParseDuration(timeout); err == nil {
cfg.Sync.Timeout = parsed
}
}
cfg.Sync.WarnFiles = getenvInt("CRABBOX_SYNC_WARN_FILES", cfg.Sync.WarnFiles)
cfg.Sync.WarnBytes = int64(getenvInt("CRABBOX_SYNC_WARN_BYTES", int(cfg.Sync.WarnBytes)))
cfg.Sync.FailFiles = getenvInt("CRABBOX_SYNC_FAIL_FILES", cfg.Sync.FailFiles)
cfg.Sync.FailBytes = int64(getenvInt("CRABBOX_SYNC_FAIL_BYTES", int(cfg.Sync.FailBytes)))
if value, ok := getenvBool("CRABBOX_SYNC_ALLOW_LARGE"); ok {
cfg.Sync.AllowLarge = value
}
cfg.Sync.BaseRef = getenv("CRABBOX_SYNC_BASE_REF", cfg.Sync.BaseRef)
if envAllow := os.Getenv("CRABBOX_ENV_ALLOW"); envAllow != "" {
cfg.EnvAllow = splitCommaList(envAllow)
}
}
func expandUserPath(path string) string {
if path == "~" {
home, _ := os.UserHomeDir()
if home != "" {
return home
}
}
if strings.HasPrefix(path, "~/") {
home, _ := os.UserHomeDir()
if home != "" {
return filepath.Join(home, strings.TrimPrefix(path, "~/"))
}
}
return path
}
func serverTypeForClass(class string) string {
return serverTypeCandidatesForClass(class)[0]
}
func serverTypeForConfig(cfg Config) string {
if isBlacksmithProvider(cfg.Provider) || isStaticProvider(cfg.Provider) || cfg.Provider == "islo" {
return ""
}
if cfg.Provider == "daytona" {
return "snapshot"
}
if cfg.Provider == "aws" {
return awsInstanceTypeCandidatesForConfig(cfg)[0]
}
if cfg.Provider == "azure" {
return azureVMSizeCandidatesForConfig(cfg)[0]
}
return serverTypeForClass(cfg.Class)
}
func serverTypeForProviderClass(provider, class string) string {
if isBlacksmithProvider(provider) || isStaticProvider(provider) || provider == "islo" {
return ""
}
if provider == "daytona" {
return "snapshot"
}
if provider == "aws" {
return awsInstanceTypeCandidatesForClass(class)[0]
}
if provider == "azure" {
return azureVMSizeCandidatesForClass(class)[0]
}
return serverTypeForClass(class)
}
func serverTypeCandidatesForClass(class string) []string {
switch class {
case "standard":
return []string{"ccx33", "cpx62", "cx53"}
case "fast":
return []string{"ccx43", "cpx62", "cx53"}
case "large":
return []string{"ccx53", "ccx43", "cpx62", "cx53"}
case "beast":
return []string{"ccx63", "ccx53", "ccx43", "cpx62", "cx53"}
default:
return []string{class}
}
}
func awsInstanceTypeCandidatesForTargetClass(target, class string) []string {
return awsInstanceTypeCandidatesForTargetModeClass(target, windowsModeNormal, class)
}
func awsInstanceTypeCandidatesForConfig(cfg Config) []string {
return awsInstanceTypeCandidatesForTargetModeClass(cfg.TargetOS, cfg.WindowsMode, cfg.Class)
}
func awsInstanceTypeCandidatesForTargetModeClass(target, windowsMode, class string) []string {
switch target {
case targetMacOS:
return []string{"mac2.metal"}
case targetWindows:
if windowsMode == windowsModeWSL2 {
switch class {
case "standard":
return []string{"m8i.large", "m8i-flex.large", "c8i.large", "r8i.large"}
case "fast":
return []string{"m8i.xlarge", "m8i-flex.xlarge", "c8i.xlarge", "r8i.xlarge"}
case "large":
return []string{"m8i.2xlarge", "m8i-flex.2xlarge", "c8i.2xlarge", "r8i.2xlarge"}
case "beast":
return []string{"m8i.4xlarge", "m8i-flex.4xlarge", "c8i.4xlarge", "r8i.4xlarge", "m8i.2xlarge"}
default:
return []string{class}
}
}
switch class {
case "standard":
return []string{"m7i.large", "m7a.large", "t3.large"}
case "fast":
return []string{"m7i.xlarge", "m7a.xlarge", "t3.xlarge"}
case "large":
return []string{"m7i.2xlarge", "m7a.2xlarge", "t3.2xlarge"}
case "beast":
return []string{"m7i.4xlarge", "m7a.4xlarge", "m7i.2xlarge"}
default:
return []string{class}
}
default:
return awsInstanceTypeCandidatesForClass(class)
}
}
func awsInstanceTypeCandidatesForClass(class string) []string {
switch class {
case "standard":
return []string{"c7a.8xlarge", "c7i.8xlarge", "m7a.8xlarge", "m7i.8xlarge", "c7a.4xlarge"}
case "fast":
return []string{"c7a.16xlarge", "c7i.16xlarge", "m7a.16xlarge", "m7i.16xlarge", "c7a.12xlarge", "c7a.8xlarge"}
case "large":
return []string{"c7a.24xlarge", "c7i.24xlarge", "m7a.24xlarge", "m7i.24xlarge", "r7a.24xlarge", "c7a.16xlarge", "c7a.12xlarge"}
case "beast":
return []string{"c7a.48xlarge", "c7i.48xlarge", "m7a.48xlarge", "m7i.48xlarge", "r7a.48xlarge", "c7a.32xlarge", "c7i.32xlarge", "m7a.32xlarge", "c7a.24xlarge", "c7a.16xlarge"}
default:
return []string{class}
}
}
func getenv(name, fallback string) string {
if v := os.Getenv(name); v != "" {
return v
}
return fallback
}
func getenvInt(name string, fallback int) int {
v := os.Getenv(name)
if v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil {
return fallback
}
return n
}
func getenvBool(name string) (bool, bool) {
v := strings.TrimSpace(os.Getenv(name))
if v == "" {
return false, false
}
switch strings.ToLower(v) {
case "1", "true", "yes", "on":
return true, true
case "0", "false", "no", "off":
return false, true
default:
return false, false
}
}
func getenvList(name string) ([]string, bool) {
value, ok := os.LookupEnv(name)
if !ok {
return nil, false
}
if strings.EqualFold(strings.TrimSpace(value), "none") {
return []string{}, true
}
return splitCommaList(value), true
}
func splitCommaList(value string) []string {
parts := strings.Split(value, ",")
return normalizeList(parts)
}
func normalizeList(values []string) []string {
out := make([]string, 0, len(values))
for _, part := range values {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
return out
}
func appendUniqueStrings(values []string, extra ...string) []string {
seen := map[string]bool{}
out := make([]string, 0, len(values)+len(extra))
for _, value := range append(values, extra...) {
value = strings.TrimSpace(value)
if value == "" || seen[value] {
continue
}
seen[value] = true
out = append(out, value)
}
return out
}