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 }