Adds AddIngressPath and RemoveIngressPath to the Deployer interface for managing per-component ingress rules in monorepo projects. - Implement conflict retry logic for concurrent ingress updates - Add K8s client interface for testability - Add comprehensive unit tests for ingress path operations - Add component deployment and teardown methods to ComponentService - Update service templates with OpenAPI spec improvements - Add evolving-app cookbook tree for reference - Split resources.go into resources_ingress.go for path-based routing - Split component.go into component_deploy.go for deployment helpers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
110 lines
3.3 KiB
Go
110 lines
3.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
)
|
|
|
|
// createInitialComponentDeployment creates a K8s Deployment for a newly added component.
|
|
// This ensures the deployment exists before CI runs, so kubectl set image succeeds.
|
|
// For monorepo projects, updates the project's unified Ingress with path-based routing.
|
|
// Failures are logged but don't fail the component creation.
|
|
func (s *ComponentService) createInitialComponentDeployment(
|
|
ctx context.Context,
|
|
projectID, projectDomain string,
|
|
component *domain.Component,
|
|
) {
|
|
// Skip if no deployer or component doesn't need a deployment
|
|
if s.deployer == nil || !component.Type.NeedsPort() {
|
|
return
|
|
}
|
|
|
|
// Build the image path - uses "latest" as placeholder until CI builds a real image
|
|
image := fmt.Sprintf("%s/%s/%s:latest", s.registryURL, projectID, component.Name)
|
|
|
|
// Assign URL path based on component type
|
|
basePath := assignComponentPath(component)
|
|
|
|
spec := domain.DeploySpec{
|
|
ProjectName: projectID,
|
|
ComponentPath: component.Path,
|
|
Image: image,
|
|
Domain: projectDomain,
|
|
Port: component.Port,
|
|
Replicas: 1,
|
|
BasePath: basePath,
|
|
}
|
|
|
|
// Create Deployment and Service (without Ingress - we manage that separately)
|
|
if err := s.deployer.Deploy(ctx, spec); err != nil {
|
|
s.logger.Warn("failed to create initial component deployment",
|
|
"project", projectID,
|
|
"component", component.Name,
|
|
"error", err,
|
|
)
|
|
return
|
|
}
|
|
|
|
// Add path to project's unified Ingress
|
|
serviceName := spec.DeploymentName()
|
|
if err := s.deployer.AddIngressPath(ctx, projectID, projectDomain, basePath, serviceName, component.Port); err != nil {
|
|
s.logger.Warn("failed to add ingress path for component",
|
|
"project", projectID,
|
|
"component", component.Name,
|
|
"path", basePath,
|
|
"error", err,
|
|
)
|
|
// Continue anyway - the deployment/service exist and CI will work
|
|
}
|
|
|
|
s.logger.Info("created initial component deployment",
|
|
"project", projectID,
|
|
"component", component.Name,
|
|
"image", image,
|
|
"path", basePath,
|
|
)
|
|
}
|
|
|
|
// assignComponentPath determines the URL path for a component.
|
|
// Services get /api/{name}, apps get /.
|
|
//
|
|
// LIMITATION: All app components (app-react, app-astro, app-nextjs) get "/" path.
|
|
// If a project has multiple apps, they will conflict on the same path.
|
|
// Future enhancement: track assigned paths and use /apps/{name} for additional apps.
|
|
func assignComponentPath(component *domain.Component) string {
|
|
switch {
|
|
case component.Type == domain.ComponentTypeService:
|
|
return "/api/" + component.Name
|
|
case component.Type.IsAppComponent():
|
|
return "/"
|
|
default:
|
|
return "" // Workers, CLI don't get HTTP paths
|
|
}
|
|
}
|
|
|
|
// assignPort finds the next available port for a component type.
|
|
func (s *ComponentService) assignPort(ctx context.Context, projectID string, componentType domain.ComponentType) (int, error) {
|
|
// Get existing components to find the highest used port
|
|
components, err := s.ListComponents(ctx, projectID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
startingPort := componentType.StartingPort()
|
|
if startingPort == 0 {
|
|
return 0, nil // Component type doesn't need a port
|
|
}
|
|
|
|
maxPort := startingPort - 1
|
|
for _, c := range components {
|
|
// Only consider components that share the same port range
|
|
if c.Type.StartingPort() == startingPort && c.Port > maxPort {
|
|
maxPort = c.Port
|
|
}
|
|
}
|
|
|
|
return maxPort + 1, nil
|
|
}
|