package main import ( "context" "log/slog" "os" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/envutil" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/internal/service" "github.com/orchard9/rdev/internal/worker" ) // Config holds application configuration. type Config struct { Port int DBHost string DBPort int DBUser string DBPassword string DBName string DBSSLMode string AdminKey string // Credential store encryption key (required for storing secrets in DB) CredentialEncryptionKey string // OpenCode configuration (optional - enables OpenCode as alternative code agent) OpenCodeURL string // e.g., "http://opencode:4096" OpenCodeUsername string // Basic auth username (default: "opencode") OpenCodePassword string // Basic auth password // Internal API token for service-to-service callbacks InternalToken string // Infrastructure adapters (threesix.ai) - fallback values if not in credential store GiteaURL string GiteaToken string GiteaDefaultOrg string CloudflareToken string CloudflareZoneID string DefaultDomain string DeployNamespace string DeployTLSIssuer string ClusterIP string RegistryURL string WoodpeckerURL string WoodpeckerAPIToken string WoodpeckerWebhookSecret string } // InfraConfig holds infrastructure adapter configuration. // Loaded from credential store with env var fallback. type InfraConfig struct { GiteaURL string GiteaToken string GiteaDefaultOrg string CloudflareToken string CloudflareZoneID string DefaultDomain string DeployNamespace string DeployTLSIssuer string ClusterIP string RegistryURL string WoodpeckerURL string WoodpeckerAPIToken string WoodpeckerWebhookSecret string // CockroachDB provisioner (for project databases) CRDBHost string // e.g., "cockroachdb-public.databases.svc.cluster.local" CRDBPort int // e.g., 26257 CRDBUser string // e.g., "root" (insecure mode) CRDBSSLMode string // e.g., "disable" (insecure) or "verify-full" (production) // Redis provisioner (for project cache) RedisHost string // e.g., "redis.databases.svc.cluster.local" RedisPort int // e.g., 6379 RedisPassword string // admin password for ACL management // GCS provisioner (for project storage) GCSProjectID string // e.g., "threesix-prod" GCSCredentialsPath string // Path to service account JSON (empty = ADC) GCSLocation string // Bucket location (default: "US") } func loadConfig() Config { return Config{ Port: envutil.GetEnvInt("PORT", 8080), DBHost: envutil.GetEnv("DB_HOST", "postgres.databases.svc.cluster.local"), DBPort: envutil.GetEnvInt("DB_PORT", 5432), DBUser: envutil.GetEnv("DB_USER", "appuser"), DBPassword: os.Getenv("DB_PASSWORD"), DBName: envutil.GetEnv("DB_NAME", "rdev"), DBSSLMode: envutil.GetEnv("DB_SSL_MODE", "disable"), AdminKey: os.Getenv("RDEV_ADMIN_KEY"), // Encryption key for credential store (generate with: openssl rand -base64 32) // REQUIRED in production - no default to prevent insecure deployments CredentialEncryptionKey: os.Getenv("CREDENTIAL_ENCRYPTION_KEY"), // OpenCode (optional alternative code agent) OpenCodeURL: os.Getenv("OPENCODE_URL"), // e.g., "http://opencode:4096" OpenCodeUsername: envutil.GetEnv("OPENCODE_USERNAME", "opencode"), OpenCodePassword: os.Getenv("OPENCODE_PASSWORD"), // Internal API token for service-to-service callbacks (e.g., SDLC callbacks) InternalToken: os.Getenv("INTERNAL_TOKEN"), // Infrastructure adapters (fallback if not in credential store) GiteaURL: envutil.GetEnv("GITEA_URL", "https://git.threesix.ai"), GiteaToken: os.Getenv("GITEA_TOKEN"), GiteaDefaultOrg: envutil.GetEnv("GITEA_DEFAULT_ORG", "jordan"), CloudflareToken: os.Getenv("CLOUDFLARE_API_TOKEN"), CloudflareZoneID: os.Getenv("CLOUDFLARE_ZONE_ID"), DefaultDomain: envutil.GetEnv("DEFAULT_DOMAIN", "threesix.ai"), DeployNamespace: envutil.GetEnv("DEPLOY_NAMESPACE", "projects"), DeployTLSIssuer: envutil.GetEnv("DEPLOY_TLS_ISSUER", "letsencrypt-prod"), ClusterIP: envutil.GetEnv("CLUSTER_IP", "208.122.204.172"), RegistryURL: envutil.GetEnv("REGISTRY_URL", "registry.threesix.ai"), WoodpeckerURL: envutil.GetEnv("WOODPECKER_URL", "https://ci.threesix.ai"), WoodpeckerAPIToken: os.Getenv("WOODPECKER_API_TOKEN"), WoodpeckerWebhookSecret: os.Getenv("WOODPECKER_WEBHOOK_SECRET"), } } // loadInfraConfig loads infrastructure configuration from credential store, // falling back to environment variables if not found in the store. func loadInfraConfig(ctx context.Context, store port.CredentialStore, cfg Config, logger *slog.Logger) InfraConfig { // Try to load from credential store creds, err := store.GetMultiple(ctx, []string{ domain.CredKeyGiteaToken, domain.CredKeyGiteaURL, domain.CredKeyCloudflareAPIToken, domain.CredKeyCloudflareZoneID, domain.CredKeyWoodpeckerURL, domain.CredKeyWoodpeckerAPIToken, domain.CredKeyWoodpeckerWebhookSecret, domain.CredKeyRegistryURL, }) if err != nil { logger.Warn("failed to load credentials from store, using env vars", "error", err) creds = make(map[string]string) } // Helper to get from store or fall back to env var getOrFallback := func(key, envFallback string) string { if v, ok := creds[key]; ok && v != "" { return v } return envFallback } infraCfg := InfraConfig{ GiteaURL: getOrFallback(domain.CredKeyGiteaURL, cfg.GiteaURL), GiteaToken: getOrFallback(domain.CredKeyGiteaToken, cfg.GiteaToken), GiteaDefaultOrg: cfg.GiteaDefaultOrg, // Not a secret, use env CloudflareToken: getOrFallback(domain.CredKeyCloudflareAPIToken, cfg.CloudflareToken), CloudflareZoneID: getOrFallback(domain.CredKeyCloudflareZoneID, cfg.CloudflareZoneID), DefaultDomain: cfg.DefaultDomain, // Not a secret, use env DeployNamespace: cfg.DeployNamespace, // Not a secret, use env DeployTLSIssuer: cfg.DeployTLSIssuer, // Not a secret, use env ClusterIP: cfg.ClusterIP, // Not a secret, use env RegistryURL: getOrFallback(domain.CredKeyRegistryURL, cfg.RegistryURL), WoodpeckerURL: getOrFallback(domain.CredKeyWoodpeckerURL, cfg.WoodpeckerURL), WoodpeckerAPIToken: getOrFallback(domain.CredKeyWoodpeckerAPIToken, cfg.WoodpeckerAPIToken), WoodpeckerWebhookSecret: getOrFallback(domain.CredKeyWoodpeckerWebhookSecret, cfg.WoodpeckerWebhookSecret), // CockroachDB and Redis provisioners (env-only for now) CRDBHost: os.Getenv("CRDB_HOST"), // e.g., "cockroachdb-public.databases.svc.cluster.local" CRDBPort: envutil.GetEnvInt("CRDB_PORT", 26257), CRDBUser: envutil.GetEnv("CRDB_USER", "root"), CRDBSSLMode: envutil.GetEnv("CRDB_SSL_MODE", "disable"), RedisHost: os.Getenv("REDIS_HOST"), // e.g., "redis.databases.svc.cluster.local" RedisPort: envutil.GetEnvInt("REDIS_PORT", 6379), RedisPassword: os.Getenv("REDIS_PASSWORD"), // GCS provisioner (env-only) GCSProjectID: os.Getenv("GCS_PROJECT_ID"), GCSCredentialsPath: os.Getenv("GCS_CREDENTIALS_PATH"), GCSLocation: envutil.GetEnv("GCS_LOCATION", "US"), } // Log which credentials were loaded from store vs env fromStore := 0 for k := range creds { if creds[k] != "" { fromStore++ } } if fromStore > 0 { logger.Info("loaded credentials from store", "count", fromStore) } return infraCfg } // closeProvisioner attempts to close a provisioner that implements io.Closer. func closeProvisioner(p any, name string, logger *slog.Logger) { if p == nil { return } if closer, ok := p.(interface{ Close() error }); ok { if err := closer.Close(); err != nil { logger.Warn("failed to close "+name+" provisioner", "error", err) } } } // podGitCommitterAdapter wraps worker.PodGitOperations to satisfy // service.PodGitCommitter without creating an import cycle. type podGitCommitterAdapter struct { podGitOps *worker.PodGitOperations } func (a *podGitCommitterAdapter) CommitAndPush(ctx context.Context, podName, workDir, message string, push bool) *service.GitCommitResult { result := a.podGitOps.CommitAndPush(ctx, podName, workDir, message, push) return &service.GitCommitResult{ HasChanges: result.HasChanges, CommitSHA: result.CommitSHA, FilesChanged: result.FilesChanged, Pushed: result.Pushed, Error: result.Error, } }