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>
214 lines
7.3 KiB
Go
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
|
|
}
|