rdev/cmd/rdev-api/config.go
jordan c59d348040 chore: prepare for composable monorepo template implementation
This commit captures the current state before implementing the composable
monorepo template system. Key changes included:

Infrastructure:
- Add CockroachDB provisioner adapter for database provisioning
- Add Redis provisioner adapter for cache provisioning
- Add build events system with PostgreSQL storage
- Add WebSocket endpoint for real-time build progress

Code agent improvements:
- Fix Claude Code adapter to use default allowed tools instead of dangerously-skip-permissions
- Add context-aware stream closing for cancellation support
- Improve parser tests for edge cases

Build system:
- Add build event constants and metrics
- Remove deprecated git_operations.go (replaced by pod_git_operations.go)
- Add rollback logic for multi-step provisioning operations

Documentation:
- Add composable-monorepo feature documentation
- Add DNS/Cloudflare service documentation
- Update deployment and troubleshooting guides

Cookbooks:
- Add fullstack-app cookbook
- Refactor landing-test with shared library

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:39:28 -07:00

214 lines
7.3 KiB
Go

package main
import (
"context"
"log/slog"
"os"
"strconv"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/port"
)
// 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
// 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"
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.threesix.svc"
RedisPort int // e.g., 6379
RedisPassword string // admin password for ACL management
}
func loadConfig() Config {
port := 8080
if v := os.Getenv("PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
port = p
}
}
dbPort := 5432
if v := os.Getenv("DB_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
dbPort = p
}
}
return Config{
Port: port,
DBHost: getEnv("DB_HOST", "postgres.databases.svc"),
DBPort: dbPort,
DBUser: getEnv("DB_USER", "appuser"),
DBPassword: os.Getenv("DB_PASSWORD"),
DBName: getEnv("DB_NAME", "rdev"),
DBSSLMode: 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: getEnv("OPENCODE_USERNAME", "opencode"),
OpenCodePassword: os.Getenv("OPENCODE_PASSWORD"),
// Infrastructure adapters (fallback if not in credential store)
GiteaURL: getEnv("GITEA_URL", "https://git.threesix.ai"),
GiteaToken: os.Getenv("GITEA_TOKEN"),
GiteaDefaultOrg: getEnv("GITEA_DEFAULT_ORG", "jordan"),
CloudflareToken: os.Getenv("CLOUDFLARE_API_TOKEN"),
CloudflareZoneID: os.Getenv("CLOUDFLARE_ZONE_ID"),
DefaultDomain: getEnv("DEFAULT_DOMAIN", "threesix.ai"),
DeployNamespace: getEnv("DEPLOY_NAMESPACE", "projects"),
DeployTLSIssuer: getEnv("DEPLOY_TLS_ISSUER", "letsencrypt-prod"),
ClusterIP: getEnv("CLUSTER_IP", "208.122.204.172"),
RegistryURL: getEnv("REGISTRY_URL", "zot.threesix.svc.cluster.local:5000"),
WoodpeckerURL: getEnv("WOODPECKER_URL", "https://ci.threesix.ai"),
WoodpeckerAPIToken: os.Getenv("WOODPECKER_API_TOKEN"),
WoodpeckerWebhookSecret: os.Getenv("WOODPECKER_WEBHOOK_SECRET"),
}
}
func getEnv(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}
// 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
}
// Parse CRDB and Redis ports
crdbPort := 26257
if v := os.Getenv("CRDB_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
crdbPort = p
}
}
redisPort := 6379
if v := os.Getenv("REDIS_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
redisPort = p
}
}
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"
CRDBPort: crdbPort,
CRDBUser: getEnv("CRDB_USER", "root"),
CRDBSSLMode: getEnv("CRDB_SSL_MODE", "disable"),
RedisHost: os.Getenv("REDIS_HOST"), // e.g., "redis.threesix.svc"
RedisPort: redisPort,
RedisPassword: os.Getenv("REDIS_PASSWORD"),
}
// 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
}