Files
2026-02-10 20:48:24 -06:00

144 lines
4.0 KiB
Go

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
}