package service import ( "context" "fmt" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" ) // addInfraComponent provisions an infrastructure component (postgres, redis). // Unlike code components, these don't scaffold files - they provision resources. func (s *ComponentService) addInfraComponent(ctx context.Context, projectID string, componentType domain.ComponentType, name string) (*domain.Component, error) { switch componentType { case domain.ComponentTypePostgres: return s.provisionPostgres(ctx, projectID, name) case domain.ComponentTypeRedis: return s.provisionRedis(ctx, projectID, name) default: return nil, fmt.Errorf("%w: unknown infrastructure type %s", domain.ErrInvalidComponentType, componentType) } } // provisionPostgres provisions a PostgreSQL/CockroachDB database for the project. func (s *ComponentService) provisionPostgres(ctx context.Context, projectID, name string) (*domain.Component, error) { if s.dbProvisioner == nil { return nil, fmt.Errorf("database provisioner not configured") } // Check if database already exists for this project existing, err := s.dbProvisioner.GetProjectDatabase(ctx, projectID) if err != nil { return nil, fmt.Errorf("failed to check existing database: %w", err) } if existing != nil { return nil, fmt.Errorf("%w: postgres already provisioned for project %s", domain.ErrDuplicateComponent, projectID) } // Provision the database creds, err := s.dbProvisioner.CreateProjectDatabase(ctx, projectID) if err != nil { return nil, fmt.Errorf("failed to provision database: %w", err) } // Store credentials if credential store is available log := logging.FromContext(ctx).WithService("component") if s.credentialStore != nil { if err := s.storeCredential(ctx, projectID, "database", "DATABASE_URL", creds.URL); err != nil { // Rollback on credential storage failure log.Error("failed to store DATABASE_URL, rolling back", logging.FieldError, err) if rollbackErr := s.dbProvisioner.DeleteProjectDatabase(ctx, projectID); rollbackErr != nil { log.Error("failed to rollback database", logging.FieldError, rollbackErr) } return nil, fmt.Errorf("failed to store credentials: %w", err) } if err := s.storeCredential(ctx, projectID, "database", "DATABASE_URL_STAGING", creds.URLStaging); err != nil { log.Warn("failed to store DATABASE_URL_STAGING", logging.FieldError, err) } } log.Info("postgres component provisioned", logging.FieldProjectID, projectID, "name", name, "database", creds.DatabaseName, ) return &domain.Component{ Type: domain.ComponentTypePostgres, Name: name, Path: "infra/postgres", Port: creds.Port, Template: "postgres", Dependencies: []string{}, }, nil } // provisionRedis provisions a Redis cache for the project. func (s *ComponentService) provisionRedis(ctx context.Context, projectID, name string) (*domain.Component, error) { if s.cacheProvisioner == nil { return nil, fmt.Errorf("cache provisioner not configured") } // Check if cache already exists for this project existing, err := s.cacheProvisioner.GetProjectCache(ctx, projectID) if err != nil { return nil, fmt.Errorf("failed to check existing cache: %w", err) } if existing != nil { return nil, fmt.Errorf("%w: redis already provisioned for project %s", domain.ErrDuplicateComponent, projectID) } // Provision the cache creds, err := s.cacheProvisioner.CreateProjectCache(ctx, projectID) if err != nil { return nil, fmt.Errorf("failed to provision cache: %w", err) } // Store credentials if credential store is available log := logging.FromContext(ctx).WithService("component") if s.credentialStore != nil { if err := s.storeCredential(ctx, projectID, "cache", "REDIS_URL", creds.URL); err != nil { // Rollback on credential storage failure log.Error("failed to store REDIS_URL, rolling back", logging.FieldError, err) if rollbackErr := s.cacheProvisioner.DeleteProjectCache(ctx, projectID, false); rollbackErr != nil { log.Error("failed to rollback cache", logging.FieldError, rollbackErr) } return nil, fmt.Errorf("failed to store credentials: %w", err) } if err := s.storeCredential(ctx, projectID, "cache", "REDIS_URL_STAGING", creds.URLStaging); err != nil { log.Warn("failed to store REDIS_URL_STAGING", logging.FieldError, err) } if err := s.storeCredential(ctx, projectID, "cache", "REDIS_PREFIX", creds.Prefix); err != nil { log.Warn("failed to store REDIS_PREFIX", logging.FieldError, err) } } log.Info("redis component provisioned", logging.FieldProjectID, projectID, "name", name, "prefix", creds.Prefix, ) return &domain.Component{ Type: domain.ComponentTypeRedis, Name: name, Path: "infra/redis", Port: creds.Port, Template: "redis", Dependencies: []string{}, }, nil } // storeCredential stores a project-scoped credential. func (s *ComponentService) storeCredential(ctx context.Context, projectID, category, key, value string) error { scopedKey := projectID + ":" + key return s.credentialStore.Set(ctx, domain.Credential{ Key: scopedKey, Value: value, Category: category, }) }