rdev/internal/service/component_infra.go
jordan adcea2fc1f
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix(templates): upgrade Go to 1.25 and fix Woodpecker syntax
## Template Version Alignment
- Go: 1.23 → 1.25 across all templates (go.work, go.mod, Dockerfiles, CI)
- Alpine: latest → 3.19 (explicit version pinning)
- Woodpecker: failure:retry → failure:ignore (invalid syntax fix)

## SDLC Tree Fixes (slackpath-5-full-lifecycle)
Fixed merge failures by correcting lifecycle flow:

1. **Branch Creation**: Added missing create-branch step (planned → ready)
   - Bug: Merge command requires feature.Branch field to be set
   - Fix: POST /projects/{id}/sdlc/features/{slug}/branch

2. **Artifact Status**: Changed approval to pass for execution artifacts
   - Bug: Review/audit/QA need status="passed" not "approved"
   - Fix: /artifacts/{type}/approve → /artifacts/{type}/pass
   - Added: pass-qa step after wait-qa

3. **Phase Transition Order**: Reordered merge phase transition
   - Bug: Merge command checks if phase == "merge" first
   - Fix: transition-to-merge BEFORE merge-feature (not after)

## GCS Provisioner Fix
- Replaced deprecated option.WithCredentialsFile with env var approach
- Now uses GOOGLE_APPLICATION_CREDENTIALS for ADC (Application Default Credentials)
- Avoids security risk from deprecated credential options
- Fixed test: Added ComponentTypeGCS to ValidComponentTypes test

## Critical Rules Added
- Version alignment: All template versions must stay in sync
- When updating versions, grep entire templates/ tree

## Files Changed
- 27 template files: Go version + Woodpecker syntax
- 1 tree file: SDLC lifecycle flow corrections
- 1 CLAUDE.md: Version alignment rule
- 1 GCS provisioner: Deprecated API fix
- 1 test file: Added missing component type

Root cause: Skeleton templates lagged behind Go 1.25 release and had
invalid Woodpecker syntax. SDLC tree skipped required branch creation
and used wrong artifact approval endpoints.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 23:57:38 -07:00

197 lines
7.1 KiB
Go

package service
import (
"context"
"fmt"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/logging"
)
// addInfraComponent provisions an infrastructure component (postgres, redis, gcs).
// 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)
case domain.ComponentTypeGCS:
return s.provisionGCS(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
}
// provisionGCS provisions a GCS bucket for the project.
func (s *ComponentService) provisionGCS(ctx context.Context, projectID, name string) (*domain.Component, error) {
if s.storageProvisioner == nil {
return nil, fmt.Errorf("storage provisioner not configured")
}
// Check if bucket already exists for this project
existing, err := s.storageProvisioner.GetProjectBucket(ctx, projectID)
if err != nil {
return nil, fmt.Errorf("failed to check existing bucket: %w", err)
}
if existing != nil {
return nil, fmt.Errorf("%w: gcs already provisioned for project %s", domain.ErrDuplicateComponent, projectID)
}
// Provision the bucket
creds, err := s.storageProvisioner.CreateProjectBucket(ctx, projectID)
if err != nil {
return nil, fmt.Errorf("failed to provision storage: %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, "storage", domain.CredKeyGCSBucket, creds.BucketName); err != nil {
// Rollback on credential storage failure
log.Error("failed to store GCS_BUCKET, rolling back", logging.FieldError, err)
if rollbackErr := s.storageProvisioner.DeleteProjectBucket(ctx, projectID, true); rollbackErr != nil {
log.Error("failed to rollback bucket", logging.FieldError, rollbackErr)
}
return nil, fmt.Errorf("failed to store credentials: %w", err)
}
if err := s.storeCredential(ctx, projectID, "storage", domain.CredKeyGCSServiceAccountJSON, creds.ServiceAccountJSON); err != nil {
log.Warn("failed to store GCS_SERVICE_ACCOUNT_JSON", logging.FieldError, err)
}
}
log.Info("gcs component provisioned",
logging.FieldProjectID, projectID,
"name", name,
"bucket", creds.BucketName,
)
return &domain.Component{
Type: domain.ComponentTypeGCS,
Name: name,
Path: "infra/gcs",
Port: 0,
Template: "gcs",
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,
})
}