package k8s import ( "fmt" "log/slog" "os" "path/filepath" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) // Client wraps a Kubernetes clientset with config used for template generation. type Client struct { Clientset kubernetes.Interface Dynamic dynamic.Interface Config Config } // Config holds cluster-specific settings for pod template generation. type Config struct { Domain string // e.g. "spinoff.dev" Registry string // e.g. "10.22.0.56:30500" GoldenImage string // e.g. "dev-golden:v2" VPNGatewayNS string // e.g. "claw-system" VPNGatewaySecret string // e.g. "vpn-gateway-secrets" AnthropicKey string // baseroute anthropic key OpenAIKey string // baseroute openai key ForgejoURL string // e.g. "http://forgejo.dev-infra.svc:3000" DevPodAPIURL string // e.g. "http://dev-pod-api.dev-infra.svc:8080" } // ConfigFromEnv reads Config from environment variables with defaults. func ConfigFromEnv() Config { return Config{ Domain: envOrDefault("DOMAIN", "spinoff.dev"), Registry: envOrDefault("REGISTRY", "10.22.0.56:30500"), GoldenImage: envOrDefault("GOLDEN_IMAGE", "dev-golden:v2"), VPNGatewayNS: envOrDefault("VPN_GATEWAY_NS", "claw-system"), VPNGatewaySecret: envOrDefault("VPN_GATEWAY_SECRET", "vpn-gateway-secrets"), AnthropicKey: os.Getenv("DEFAULT_ANTHROPIC_KEY"), OpenAIKey: os.Getenv("DEFAULT_OPENAI_KEY"), ForgejoURL: envOrDefault("FORGEJO_URL", "http://forgejo.dev-infra.svc:3000"), DevPodAPIURL: envOrDefault("DEV_POD_API_URL", "http://dev-pod-api.dev-infra.svc:8080"), } } // NewClient creates a Kubernetes client using in-cluster config, // falling back to kubeconfig for local development. func NewClient(cfg Config) (*Client, error) { restCfg, err := rest.InClusterConfig() if err != nil { slog.Info("in-cluster config not available, falling back to kubeconfig") restCfg, err = kubeconfigFromDefault() if err != nil { return nil, fmt.Errorf("create k8s config: %w", err) } } clientset, err := kubernetes.NewForConfig(restCfg) if err != nil { return nil, fmt.Errorf("create k8s clientset: %w", err) } dyn, err := dynamic.NewForConfig(restCfg) if err != nil { return nil, fmt.Errorf("create dynamic client: %w", err) } return &Client{ Clientset: clientset, Dynamic: dyn, Config: cfg, }, nil } // NewClientWithClientset creates a Client with pre-configured clients (for testing). func NewClientWithClientset(clientset kubernetes.Interface, dyn dynamic.Interface, cfg Config) *Client { return &Client{ Clientset: clientset, Dynamic: dyn, Config: cfg, } } func kubeconfigFromDefault() (*rest.Config, error) { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("get home dir: %w", err) } kubeconfig := filepath.Join(home, ".kube", "config") return clientcmd.BuildConfigFromFlags("", kubeconfig) } func envOrDefault(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }