Implements weeks 1-4 of the multi-provider architecture: Week 1 - Foundation: - Add domain models (AgentProvider, AgentRequest, AgentEvent, AgentResult) - Define CodeAgent port interface with Execute, Cancel, Capabilities - Create thread-safe provider registry with first-registered default Week 2 - Claude Code Adapter: - Extract kubectl exec logic into CodeAgent implementation - Parse stream-json output format (init, message, tool_use, result) - Support session continuation via --resume flag Week 3 - OpenCode Adapter: - HTTP/SSE client for opencode serve API - Session management (create, send message, abort) - Event streaming with documented buffer rationale Week 4 - Quality & Polish: - Fix race condition in OpenCode Cancel method - Add AgentRequest.Validate() with ErrPromptRequired, ErrInvalidTimeout - Document DefaultAvailabilityTimeout constants - Add HTTP error context for debugging Also includes: - Work queue system with PostgreSQL adapter - Credential store for infrastructure secrets - Project templates with Woodpecker CI integration - Comprehensive test coverage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
531 lines
18 KiB
Go
531 lines
18 KiB
Go
// Package main provides the entry point for the rdev API server.
|
|
//
|
|
// rdev (Remote Developer) provides a REST API for controlling Claude Code
|
|
// instances running in Kubernetes pods. External clients (Discord bots,
|
|
// CLI tools, etc.) connect via this API.
|
|
//
|
|
// Authentication:
|
|
// - All endpoints (except /health, /ready, /docs) require X-API-Key header
|
|
// - Admin key from RDEV_ADMIN_KEY env var for key management
|
|
// - Create additional keys via POST /keys
|
|
//
|
|
// Endpoints:
|
|
// - GET /health - Health check (no auth)
|
|
// - GET /ready - Readiness check (no auth)
|
|
// - GET /docs - Scalar API documentation (no auth)
|
|
// - GET /openapi.json - OpenAPI 3.0 specification (no auth)
|
|
// - GET /keys - List API keys
|
|
// - POST /keys - Create API key
|
|
// - GET /keys/{id} - Get API key details
|
|
// - DELETE /keys/{id} - Revoke API key
|
|
// - GET /projects - List available projects
|
|
// - GET /projects/{id} - Get project details
|
|
// - POST /projects/{id}/claude - Run Claude command
|
|
// - POST /projects/{id}/shell - Run shell command
|
|
// - POST /projects/{id}/git - Run git command
|
|
// - GET /projects/{id}/events - SSE stream for output
|
|
// - GET /projects/{id}/claude-config - List commands/skills/agents
|
|
// - GET /projects/{id}/claude-config/commands - List commands
|
|
// - POST /projects/{id}/claude-config/commands - Create command
|
|
// - GET /projects/{id}/claude-config/commands/{name} - Get command
|
|
// - PUT /projects/{id}/claude-config/commands/{name} - Update command
|
|
// - DELETE /projects/{id}/claude-config/commands/{name} - Delete command
|
|
// (same pattern for /skills and /agents)
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/orchard9/rdev/internal/adapter/cloudflare"
|
|
"github.com/orchard9/rdev/internal/adapter/deployer"
|
|
"github.com/orchard9/rdev/internal/adapter/gitea"
|
|
"github.com/orchard9/rdev/internal/adapter/kubernetes"
|
|
"github.com/orchard9/rdev/internal/adapter/memory"
|
|
"github.com/orchard9/rdev/internal/adapter/postgres"
|
|
"github.com/orchard9/rdev/internal/adapter/templates"
|
|
"github.com/orchard9/rdev/internal/adapter/woodpecker"
|
|
"github.com/orchard9/rdev/internal/auth"
|
|
"github.com/orchard9/rdev/internal/db"
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/handlers"
|
|
"github.com/orchard9/rdev/internal/metrics"
|
|
"github.com/orchard9/rdev/internal/middleware"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
"github.com/orchard9/rdev/internal/service"
|
|
"github.com/orchard9/rdev/internal/telemetry"
|
|
"github.com/orchard9/rdev/internal/webhook"
|
|
"github.com/orchard9/rdev/internal/worker"
|
|
"github.com/orchard9/rdev/pkg/api"
|
|
)
|
|
|
|
func main() {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelInfo,
|
|
}))
|
|
|
|
// Initialize telemetry (OpenTelemetry)
|
|
telCfg := telemetry.DefaultConfig()
|
|
telCfg.Logger = logger
|
|
tel, err := telemetry.New(context.Background(), telCfg)
|
|
if err != nil {
|
|
logger.Error("failed to initialize telemetry", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Load configuration from environment
|
|
cfg := loadConfig()
|
|
|
|
// Validate required security configuration
|
|
if cfg.CredentialEncryptionKey == "" {
|
|
logger.Warn("CREDENTIAL_ENCRYPTION_KEY not set - credential store will use insecure default",
|
|
"hint", "Generate with: openssl rand -base64 32")
|
|
// Use a deterministic fallback for development only
|
|
cfg.CredentialEncryptionKey = "rdev-dev-key-not-for-production"
|
|
}
|
|
|
|
// Initialize database with auto-migrations
|
|
database, err := db.New(db.Config{
|
|
Host: cfg.DBHost,
|
|
Port: cfg.DBPort,
|
|
User: cfg.DBUser,
|
|
Password: cfg.DBPassword,
|
|
Database: cfg.DBName,
|
|
SSLMode: cfg.DBSSLMode,
|
|
}, logger)
|
|
if err != nil {
|
|
logger.Error("failed to connect to database", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = database.Close() }()
|
|
|
|
// Initialize auth service
|
|
authService := auth.NewService(database.DB, cfg.AdminKey)
|
|
|
|
// Initialize credential store (for infrastructure secrets)
|
|
credentialStore := postgres.NewCredentialStore(database.DB, cfg.CredentialEncryptionKey)
|
|
|
|
// Load infrastructure config from credential store (falls back to env vars)
|
|
infraCfg := loadInfraConfig(context.Background(), credentialStore, cfg, logger)
|
|
|
|
// Create adapters (dependency injection)
|
|
namespace := getEnv("K8S_NAMESPACE", "rdev")
|
|
|
|
// Initialize K8s client for dynamic project discovery
|
|
// Falls back gracefully if K8s is unavailable (e.g., local development)
|
|
k8sClient := kubernetes.NewClientOrNil(kubernetes.ClientConfig{
|
|
Namespace: namespace,
|
|
Kubeconfig: os.Getenv("KUBECONFIG"),
|
|
})
|
|
if k8sClient != nil {
|
|
logger.Info("k8s client initialized, dynamic project discovery enabled")
|
|
} else {
|
|
logger.Warn("k8s client unavailable, using hardcoded fallback projects")
|
|
}
|
|
|
|
projectRepo := kubernetes.NewProjectRepositoryWithClient(namespace, k8sClient, logger)
|
|
k8sExecutor := kubernetes.NewExecutor(namespace)
|
|
streamPub := memory.NewStreamPublisher()
|
|
|
|
// Start watching for project pod changes if K8s client is available
|
|
if k8sClient != nil {
|
|
if err := projectRepo.StartWatching(context.Background()); err != nil {
|
|
logger.Warn("failed to start project watcher", "error", err)
|
|
}
|
|
}
|
|
|
|
// Initialize audit logger
|
|
auditLogger := postgres.NewAuditLogger(database.DB)
|
|
|
|
// Initialize rate limiter
|
|
rateLimiter := postgres.NewRateLimiter(database.DB)
|
|
stopRateLimitCleanup := rateLimiter.StartCleanupWorker(context.Background(), 5*time.Minute)
|
|
|
|
// Initialize command queue
|
|
commandQueue := postgres.NewCommandQueueRepository(database.DB)
|
|
|
|
// Initialize work queue (for worker pool tasks)
|
|
workQueueRepo := postgres.NewWorkQueueRepository(database.DB)
|
|
|
|
// Initialize webhook repository and dispatcher
|
|
webhookRepo := postgres.NewWebhookRepository(database.DB)
|
|
webhookDispatcher := webhook.NewDispatcher(webhookRepo, &webhook.DispatcherConfig{
|
|
WorkerCount: 10,
|
|
MaxRetries: 3,
|
|
Timeout: 30 * time.Second,
|
|
RetryBackoff: 5 * time.Second,
|
|
Logger: logger,
|
|
})
|
|
if err := webhookDispatcher.Start(); err != nil {
|
|
logger.Error("failed to start webhook dispatcher", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Initialize infrastructure adapters (optional - only if configured)
|
|
// Uses infraCfg which loads from credential store with env var fallback
|
|
var giteaClient *gitea.Client
|
|
if infraCfg.GiteaToken != "" && infraCfg.GiteaURL != "" {
|
|
var err error
|
|
giteaClient, err = gitea.NewClient(infraCfg.GiteaURL, infraCfg.GiteaToken, infraCfg.GiteaDefaultOrg)
|
|
if err != nil {
|
|
logger.Warn("failed to initialize gitea client", "error", err)
|
|
} else {
|
|
logger.Info("gitea client initialized", "url", infraCfg.GiteaURL, "org", infraCfg.GiteaDefaultOrg)
|
|
}
|
|
}
|
|
|
|
var dnsClient *cloudflare.Client
|
|
if infraCfg.CloudflareToken != "" && infraCfg.CloudflareZoneID != "" {
|
|
dnsClient = cloudflare.NewClient(infraCfg.CloudflareToken, infraCfg.CloudflareZoneID, infraCfg.DefaultDomain)
|
|
logger.Info("cloudflare DNS client initialized", "domain", infraCfg.DefaultDomain)
|
|
}
|
|
|
|
var deployerAdapter *deployer.Deployer
|
|
if k8sClient != nil {
|
|
deployerAdapter = deployer.NewDeployer(k8sClient, deployer.Config{
|
|
Namespace: infraCfg.DeployNamespace,
|
|
IngressClass: "traefik",
|
|
TLSIssuer: infraCfg.DeployTLSIssuer,
|
|
DefaultDomain: infraCfg.DefaultDomain,
|
|
DefaultReplicas: 1,
|
|
})
|
|
logger.Info("deployer initialized", "namespace", infraCfg.DeployNamespace)
|
|
}
|
|
|
|
var woodpeckerClient *woodpecker.Client
|
|
if infraCfg.WoodpeckerURL != "" && infraCfg.WoodpeckerAPIToken != "" {
|
|
var err error
|
|
woodpeckerClient, err = woodpecker.NewClient(
|
|
infraCfg.WoodpeckerURL,
|
|
infraCfg.WoodpeckerAPIToken,
|
|
woodpecker.WithLogger(logger),
|
|
)
|
|
if err != nil {
|
|
logger.Warn("failed to initialize woodpecker client", "error", err)
|
|
} else {
|
|
logger.Info("woodpecker CI client initialized", "url", infraCfg.WoodpeckerURL)
|
|
}
|
|
}
|
|
|
|
// Initialize template provider (requires Gitea client for seeding repos)
|
|
var templateProvider *templates.Provider
|
|
if giteaClient != nil {
|
|
// Get the underlying Gitea SDK client for the template provider
|
|
templateProvider = templates.NewProvider(giteaClient.SDKClient(), logger)
|
|
logger.Info("template provider initialized")
|
|
}
|
|
|
|
// Create services
|
|
projectService := service.NewProjectService(projectRepo, k8sExecutor, streamPub).
|
|
WithAuditLogger(auditLogger).
|
|
WithCommandQueue(commandQueue).
|
|
WithWebhookDispatcher(webhookDispatcher)
|
|
|
|
// Create work service (for worker pool task management)
|
|
workService := service.NewWorkService(workQueueRepo, service.WorkServiceConfig{
|
|
Logger: logger,
|
|
}).WithWebhookDispatcher(webhookDispatcher)
|
|
|
|
// Create app
|
|
app := api.New("rdev-api",
|
|
api.WithPort(cfg.Port),
|
|
api.WithLogger(logger),
|
|
)
|
|
|
|
// Add telemetry middleware (first to capture all requests)
|
|
app.Use(telemetry.Middleware(telCfg.ServiceName))
|
|
|
|
// Add metrics middleware (before auth to track all requests)
|
|
app.Use(metrics.Middleware)
|
|
|
|
// Add auth middleware (skips /health, /ready, /docs, /openapi.json, /metrics)
|
|
app.Use(auth.Middleware(authService))
|
|
|
|
// Add rate limiting middleware (after auth, so we have API key context)
|
|
rateLimitCfg := middleware.DefaultRateLimitConfig()
|
|
rateLimitCfg.Limiter = rateLimiter
|
|
app.Use(middleware.RateLimitMiddleware(rateLimitCfg))
|
|
|
|
// Register metrics endpoint (no auth required)
|
|
app.Router().Handle("/metrics", metrics.Handler())
|
|
|
|
// Initialize handlers
|
|
projectsHandler := handlers.NewProjectsHandlerWithService(projectService)
|
|
keysHandler := handlers.NewKeysHandler(authService)
|
|
claudeConfigHandler := handlers.NewClaudeConfigHandlerWithService(projectService, projectRepo, k8sExecutor)
|
|
auditHandler := handlers.NewAuditHandler(auditLogger)
|
|
queueHandler := handlers.NewQueueHandler(commandQueue, projectRepo)
|
|
webhookHandler := handlers.NewWebhookHandler(webhookRepo, projectRepo)
|
|
workHandler := handlers.NewWorkHandler(workService)
|
|
|
|
// Initialize infrastructure handler (for threesix.ai git/deploy/dns)
|
|
infraHandler := handlers.NewInfrastructureHandler(
|
|
giteaClient,
|
|
dnsClient,
|
|
deployerAdapter,
|
|
projectRepo,
|
|
handlers.InfrastructureConfig{
|
|
DefaultGitOwner: infraCfg.GiteaDefaultOrg,
|
|
DefaultDomain: infraCfg.DefaultDomain,
|
|
},
|
|
)
|
|
|
|
// Initialize project infrastructure service (orchestrates full project lifecycle)
|
|
projectInfraService := service.NewProjectInfraService(
|
|
database.DB,
|
|
giteaClient,
|
|
dnsClient,
|
|
deployerAdapter,
|
|
woodpeckerClient, // CI provider for auto-activating repos
|
|
templateProvider, // Template provider for seeding repos
|
|
service.ProjectInfraConfig{
|
|
DefaultGitOwner: infraCfg.GiteaDefaultOrg,
|
|
DefaultDomain: infraCfg.DefaultDomain,
|
|
ClusterIP: infraCfg.ClusterIP,
|
|
Logger: logger,
|
|
},
|
|
)
|
|
|
|
// Initialize project management handler
|
|
projectMgmtHandler := handlers.NewProjectManagementHandler(projectInfraService)
|
|
|
|
// Initialize Woodpecker webhook handler (for CI/CD auto-deploy)
|
|
woodpeckerHandler := handlers.NewWoodpeckerWebhookHandler(
|
|
deployerAdapter,
|
|
dnsClient,
|
|
handlers.WoodpeckerWebhookConfig{
|
|
WebhookSecret: infraCfg.WoodpeckerWebhookSecret,
|
|
DefaultDomain: infraCfg.DefaultDomain,
|
|
RegistryURL: infraCfg.RegistryURL,
|
|
ClusterIP: infraCfg.ClusterIP,
|
|
Logger: logger,
|
|
},
|
|
)
|
|
|
|
// Initialize credentials handler (superadmin only)
|
|
credentialsHandler := handlers.NewCredentialsHandler(credentialStore)
|
|
|
|
// Register routes
|
|
projectsHandler.Mount(app.Router())
|
|
keysHandler.Mount(app.Router())
|
|
claudeConfigHandler.Mount(app.Router())
|
|
auditHandler.Mount(app.Router())
|
|
queueHandler.Mount(app.Router())
|
|
webhookHandler.Mount(app.Router())
|
|
workHandler.Mount(app.Router())
|
|
infraHandler.Mount(app.Router())
|
|
projectMgmtHandler.Mount(app.Router())
|
|
woodpeckerHandler.Mount(app.Router())
|
|
credentialsHandler.Mount(app.Router())
|
|
|
|
// Start queue processor worker
|
|
queueProcessor := worker.NewQueueProcessor(
|
|
commandQueue,
|
|
k8sExecutor,
|
|
projectRepo,
|
|
streamPub,
|
|
&worker.QueueProcessorConfig{
|
|
PollPeriod: 5 * time.Second,
|
|
Logger: logger,
|
|
},
|
|
).WithWebhookDispatcher(webhookDispatcher)
|
|
if err := queueProcessor.Start(); err != nil {
|
|
logger.Error("failed to start queue processor", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Enable API documentation
|
|
app.EnableDocs(buildOpenAPISpec())
|
|
|
|
// Cleanup on shutdown
|
|
app.OnShutdown(func(ctx context.Context) error {
|
|
// Stop queue processor
|
|
queueProcessor.Stop()
|
|
|
|
// Stop webhook dispatcher
|
|
webhookDispatcher.Stop()
|
|
|
|
// Stop project watcher
|
|
projectRepo.StopWatching()
|
|
|
|
// Stop rate limit cleanup worker
|
|
stopRateLimitCleanup()
|
|
|
|
// Shutdown telemetry (flush pending traces)
|
|
if err := tel.Shutdown(ctx); err != nil {
|
|
logger.Error("telemetry shutdown error", "error", err)
|
|
}
|
|
|
|
return database.Close()
|
|
})
|
|
|
|
logger.Info("rdev-api starting",
|
|
"port", cfg.Port,
|
|
"db_host", cfg.DBHost,
|
|
"admin_key_set", cfg.AdminKey != "",
|
|
)
|
|
|
|
app.Run()
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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
|
|
}
|
|
|
|
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"),
|
|
|
|
// 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-threesix"),
|
|
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
|
|
}
|
|
|
|
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),
|
|
}
|
|
|
|
// 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
|
|
}
|