package main import ( "context" "fmt" "os" "strings" "time" "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/client" "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/service" "go.yaml.in/yaml/v3" ) const ( repoURL = "https://tangled.org/evan.jarrett.net/at-container-registry" repoBranch = "main" privateNetworkCIDR = "10.0.1.0/24" ) // InfraConfig holds infrastructure configuration. type InfraConfig struct { Zone string Plan string SSHPublicKey string S3SecretKey string // Infrastructure naming — derived from configs/appview.yaml.tmpl. // Edit that template to rebrand. ClientName string BaseDomain string RegistryDomains []string RepoURL string RepoBranch string } // Naming returns a Naming helper derived from ClientName. func (c *InfraConfig) Naming() Naming { return Naming{ClientName: c.ClientName} } func loadConfig(zone, plan, sshKeyPath, s3Secret string) (*InfraConfig, error) { sshKey, err := readSSHPublicKey(sshKeyPath) if err != nil { return nil, err } clientName, baseDomain, registryDomains, err := extractFromAppviewTemplate() if err != nil { return nil, fmt.Errorf("extract config from template: %w", err) } return &InfraConfig{ Zone: zone, Plan: plan, SSHPublicKey: sshKey, S3SecretKey: s3Secret, ClientName: clientName, BaseDomain: baseDomain, RegistryDomains: registryDomains, RepoURL: repoURL, RepoBranch: repoBranch, }, nil } // extractFromAppviewTemplate renders the appview config template with // zero-value ConfigValues and parses the resulting YAML to extract // deployment-specific values. The template is the single source of truth. func extractFromAppviewTemplate() (clientName, baseDomain string, registryDomains []string, err error) { rendered, err := renderConfig(appviewConfigTmpl, &ConfigValues{}) if err != nil { return "", "", nil, fmt.Errorf("render appview template: %w", err) } var cfg struct { Server struct { BaseURL string `yaml:"base_url"` ClientName string `yaml:"client_name"` RegistryDomains []string `yaml:"registry_domains"` } `yaml:"server"` } if err := yaml.Unmarshal([]byte(rendered), &cfg); err != nil { return "", "", nil, fmt.Errorf("parse appview template YAML: %w", err) } clientName = strings.ToLower(cfg.Server.ClientName) baseDomain = strings.TrimPrefix(cfg.Server.BaseURL, "https://") registryDomains = cfg.Server.RegistryDomains return clientName, baseDomain, registryDomains, nil } // readSSHPublicKey reads an SSH public key from a file path. func readSSHPublicKey(path string) (string, error) { if path == "" { return "", fmt.Errorf("--ssh-key is required (path to SSH public key file)") } data, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("read SSH public key %s: %w", path, err) } key := strings.TrimSpace(string(data)) if key == "" { return "", fmt.Errorf("SSH public key file %s is empty", path) } return key, nil } // resolveInteractive fills in any empty Zone/Plan fields by launching // interactive TUI pickers that query the UpCloud API. func resolveInteractive(ctx context.Context, svc *service.Service, cfg *InfraConfig) error { if cfg.Zone == "" { z, err := pickZone(ctx, svc) if err != nil { return fmt.Errorf("zone picker: %w", err) } cfg.Zone = z } if cfg.Plan == "" { p, err := pickPlan(ctx, svc) if err != nil { return fmt.Errorf("plan picker: %w", err) } cfg.Plan = p } return nil } // newService creates an UpCloud API client. If token is non-empty it's used // directly; otherwise credentials are read from UPCLOUD_TOKEN env var. func newService(token string) (*service.Service, error) { var c *client.Client var err error if token != "" { c = client.New("", "", client.WithBearerAuth(token), client.WithTimeout(120*time.Second)) } else { c, err = client.NewFromEnv(client.WithTimeout(120 * time.Second)) if err != nil { return nil, fmt.Errorf("create UpCloud client: %w\n\nPass --token or set UPCLOUD_TOKEN", err) } } return service.New(c), nil }