rdev/internal/domain/component.go
jordan 3247ce3ca0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: worker deployments and JWT_SECRET auto-provisioning
RC-1: Workers now get a Kubernetes Deployment on component creation.
NeedsPort() (port assignment) was incorrectly used to gate Deployment
creation - workers have no HTTP port but still need a Deployment so
CI `kubectl set image` can succeed. Added NeedsDeployment() returning
true for service/worker/app-react/app-astro/app-nextjs. AddIngressPath
is now guarded by port > 0 so workers don't attempt HTTP routing.

RC-2: JWT_SECRET is now auto-provisioned per-project when the first
code component is added. The skeleton service template fatally requires
JWT_SECRET at startup; previously fetchProjectCredentials() never fetched
it. ensureProjectJWTSecret() generates a cryptographically random 32-byte
secret, stores it as "{projectID}:JWT_SECRET", and JWT_SECRET is now
included in projectScopedKeys so it's injected into every deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 03:42:53 -07:00

132 lines
4.5 KiB
Go

// Package domain contains pure domain models with no external dependencies.
package domain
import "regexp"
// ComponentType represents the type of component in a monorepo.
type ComponentType string
const (
ComponentTypeService ComponentType = "service"
ComponentTypeWorker ComponentType = "worker"
ComponentTypeAppAstro ComponentType = "app-astro"
ComponentTypeAppReact ComponentType = "app-react"
ComponentTypeAppNextJS ComponentType = "app-nextjs"
ComponentTypeCLI ComponentType = "cli"
// Infrastructure component types - these trigger provisioning, not scaffolding.
ComponentTypePostgres ComponentType = "postgres"
ComponentTypeRedis ComponentType = "redis"
ComponentTypeGCS ComponentType = "gcs"
)
// ValidComponentTypes lists all valid component types.
var ValidComponentTypes = []ComponentType{
ComponentTypeService,
ComponentTypeWorker,
ComponentTypeAppAstro,
ComponentTypeAppReact,
ComponentTypeAppNextJS,
ComponentTypeCLI,
ComponentTypePostgres,
ComponentTypeRedis,
ComponentTypeGCS,
}
// IsValidComponentType checks if a string is a valid component type.
func IsValidComponentType(t string) bool {
for _, valid := range ValidComponentTypes {
if string(valid) == t {
return true
}
}
return false
}
// Component represents a component in a monorepo project.
type Component struct {
Type ComponentType `json:"type"`
Name string `json:"name"`
Path string `json:"path"` // e.g., "services/auth-api"
Port int `json:"port"` // 0 if not applicable
Template string `json:"template"` // template used
Dependencies []string `json:"dependencies"` // e.g., ["postgres", "redis"]
}
// DestDir returns the destination directory for this component type.
func (c ComponentType) DestDir() string {
switch c {
case ComponentTypeService:
return "services"
case ComponentTypeWorker:
return "workers"
case ComponentTypeAppAstro, ComponentTypeAppReact, ComponentTypeAppNextJS:
return "apps"
case ComponentTypeCLI:
return "cli"
default:
return ""
}
}
// StartingPort returns the starting port number for this component type.
// Workers and CLIs don't expose ports (return 0).
func (c ComponentType) StartingPort() int {
switch c {
case ComponentTypeService:
return 8001
case ComponentTypeAppAstro, ComponentTypeAppReact, ComponentTypeAppNextJS:
return 3001
case ComponentTypeWorker, ComponentTypeCLI:
return 0
default:
return 0
}
}
// NeedsPort returns true if this component type requires a port assignment.
func (c ComponentType) NeedsPort() bool {
return c == ComponentTypeService || c == ComponentTypeAppAstro || c == ComponentTypeAppReact || c == ComponentTypeAppNextJS
}
// NeedsDeployment returns true if this component type requires a Kubernetes Deployment.
// All code components except CLI need a Deployment so CI can use kubectl set image.
// Workers have no HTTP port but still need a Deployment to run as background processes.
func (c ComponentType) NeedsDeployment() bool {
return c == ComponentTypeService || c == ComponentTypeWorker ||
c == ComponentTypeAppAstro || c == ComponentTypeAppReact || c == ComponentTypeAppNextJS
}
// IsGoComponent returns true if this component type uses Go (and needs go.work entry).
func (c ComponentType) IsGoComponent() bool {
return c == ComponentTypeService || c == ComponentTypeWorker || c == ComponentTypeCLI
}
// IsAppComponent returns true if this component type is a frontend app (and gets "/" path).
func (c ComponentType) IsAppComponent() bool {
return c == ComponentTypeAppAstro || c == ComponentTypeAppReact || c == ComponentTypeAppNextJS
}
// IsInfraComponent returns true if this component type is infrastructure (database, cache, storage).
// Infrastructure components trigger provisioning instead of template scaffolding.
func (c ComponentType) IsInfraComponent() bool {
return c == ComponentTypePostgres || c == ComponentTypeRedis || c == ComponentTypeGCS
}
// componentNameRegex validates component names (slug format: lowercase, alphanumeric, dashes).
var componentNameRegex = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
// ValidateComponentName validates that a component name is in slug format.
// Must be lowercase, start with a letter, and contain only letters, numbers, and dashes.
func ValidateComponentName(name string) error {
if name == "" {
return ErrInvalidComponentName
}
if len(name) > MaxProjectNameLen { // Reuse the 63-char limit from K8s
return ErrInvalidComponentName
}
if !componentNameRegex.MatchString(name) {
return ErrInvalidComponentName
}
return nil
}