package services import ( "context" "encoding/json" "fmt" "time" "git.linuxforward.com/byop/byop-engine/cloud" "git.linuxforward.com/byop/byop-engine/dbstore" "git.linuxforward.com/byop/byop-engine/models" "github.com/google/uuid" "github.com/sirupsen/logrus" ) // DeploymentService handles business logic for deployments type DeploymentService struct { store *dbstore.DeploymentStore componentStore *dbstore.ComponentStore blueprintStore *dbstore.BlueprintStore clientStore *dbstore.ClientStore logger *logrus.Entry } // NewDeploymentService creates a new DeploymentService func NewDeploymentService( store *dbstore.DeploymentStore, componentStore *dbstore.ComponentStore, blueprintStore *dbstore.BlueprintStore, clientStore *dbstore.ClientStore, ) *DeploymentService { return &DeploymentService{ store: store, componentStore: componentStore, blueprintStore: blueprintStore, clientStore: clientStore, logger: logrus.WithField("component", "deployment_service"), } } // CreateDeployment creates a new deployment func (s *DeploymentService) CreateDeployment(deployment *models.Deployment) error { // Generate UUID if not provided if deployment.ID == "" { deployment.ID = uuid.New().String() } // Validate the deployment if err := s.validateDeployment(deployment); err != nil { return fmt.Errorf("invalid deployment: %w", err) } // Set appropriate status deployment.Status = string(models.PENDING_DEPLOYMENT) // Set timestamps now := time.Now() deployment.CreatedAt = now deployment.UpdatedAt = now // Get instance from pool ctx := context.Background() return s.createDeploymentWithNewInstance(ctx, deployment) } // createDeploymentWithNewInstance creates a new instance directly from the provider func (s *DeploymentService) createDeploymentWithNewInstance(ctx context.Context, deployment *models.Deployment) error { // Get provider provider, ok := cloud.GetProvider(deployment.Provider) if !ok { return fmt.Errorf("provider %s not found", deployment.Provider) } blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID) if err != nil { return fmt.Errorf("failed to retrieve blueprint: %w", err) } if blueprint == nil { return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID) } var components []models.Component for _, component := range blueprint.Config.Components { fmt.Printf("Component ID: %s\n", component.ID) comp, err := s.componentStore.GetByID(component.ID) if err != nil { fmt.Errorf("failed to retrieve component: %w", err) continue } if comp == nil { fmt.Errorf("component with ID %s not found", component.ID) continue } components = append(components, *comp) } // Create instance options instanceOpts := cloud.InstanceCreateOpts{ Name: fmt.Sprintf("deployment-%s", deployment.ID[:8]), Components: components, Tags: map[string]string{ "managed-by": "byop-platform", "deployment-id": deployment.ID, "client-id": deployment.ClientID, // "component-id": deployment.ComponentID, "blueprint-id": deployment.BlueprintID, }, } // Create the instance s.logger.WithFields(logrus.Fields{ "name": instanceOpts.Name, "provider": deployment.Provider, "region": deployment.Region, "size": deployment.InstanceType, }).Info("Creating new instance for deployment") instance, err := provider.GetFirstFreeInstance(ctx) if err != nil { return fmt.Errorf("failed to create instance: %w", err) } // Update deployment with instance info deployment.InstanceID = instance.ID deployment.IPAddress = instance.IPAddress // Save the deployment if err := s.store.CreateDeployment(deployment); err != nil { // If we fail to save deployment, try to delete the instance _ = provider.ResetInstance(ctx, instance.ID) return fmt.Errorf("failed to save deployment: %w", err) } // Start deployment process asynchronously go s.processDeployment(ctx, deployment) return nil } // GetDeployment retrieves a deployment by ID func (s *DeploymentService) GetDeployment(id string) (*models.Deployment, error) { deployment, err := s.store.GetByID(id) if err != nil { return nil, fmt.Errorf("failed to retrieve deployment: %w", err) } if deployment != nil { // Deserialize config fields if err := s.deserializeConfigFields(deployment); err != nil { return nil, fmt.Errorf("failed to deserialize config fields: %w", err) } } return deployment, nil } // UpdateDeployment updates an existing deployment func (s *DeploymentService) UpdateDeployment(deployment *models.Deployment) error { // Validate the deployment ID if deployment.ID == "" { return fmt.Errorf("deployment ID is required for update") } // Check if deployment exists existingDeployment, err := s.store.GetByID(deployment.ID) if err != nil { return fmt.Errorf("failed to check if deployment exists: %w", err) } if existingDeployment == nil { return fmt.Errorf("deployment with ID %s not found", deployment.ID) } // Prevent updates to deployed apps if deployment is not in the right state if existingDeployment.Status != string(models.PENDING_DEPLOYMENT) && existingDeployment.Status != string(models.FAILED_DEPLOYMENT) && len(deployment.DeployedComponents) > 0 { return fmt.Errorf("cannot update deployed apps when deployment is in %s state", existingDeployment.Status) } // Validate the deployment if err := s.validateDeployment(deployment); err != nil { return fmt.Errorf("invalid deployment: %w", err) } // If status was updated to "deploying", update LastDeployedAt if existingDeployment.Status != string(models.DEPLOYING) && deployment.Status == string(models.DEPLOYING) { deployment.LastDeployedAt = time.Now() } // Handle deployed apps setup if err := s.setupDeployedComponents(deployment); err != nil { return fmt.Errorf("failed to setup deployed apps: %w", err) } // Persist the deployment if err := s.store.Update(deployment); err != nil { return fmt.Errorf("failed to update deployment: %w", err) } return nil } // DeleteDeployment deletes a deployment by ID func (s *DeploymentService) DeleteDeployment(id string) error { // Check if deployment exists deployment, err := s.store.GetByID(id) if err != nil { return fmt.Errorf("failed to check if deployment exists: %w", err) } if deployment == nil { return fmt.Errorf("deployment with ID %s not found", id) } // Set status to deleting deployment.Status = string(models.DELETING) if err := s.store.Update(deployment); err != nil { return fmt.Errorf("failed to update deployment status: %w", err) } // Trigger cleanup process (this would normally be asynchronous) // This is a placeholder for your actual cleanup logic go s.processDeploymentCleanup(id) return nil } // ListDeployments retrieves all deployments with optional filtering func (s *DeploymentService) ListDeployments(filter map[string]interface{}) ([]*models.Deployment, error) { deployments, err := s.store.List(filter) if err != nil { return nil, fmt.Errorf("failed to list deployments: %w", err) } // Deserialize config fields for each deployment for _, deployment := range deployments { if err := s.deserializeConfigFields(deployment); err != nil { return nil, fmt.Errorf("failed to deserialize config fields: %w", err) } } return deployments, nil } // GetDeploymentsByClientID retrieves deployments for a specific client func (s *DeploymentService) GetDeploymentsByClientID(clientID string) ([]*models.Deployment, error) { // Check if client exists client, err := s.clientStore.GetByID(clientID) if err != nil { return nil, fmt.Errorf("failed to check if client exists: %w", err) } if client == nil { return nil, fmt.Errorf("client with ID %s not found", clientID) } deployments, err := s.store.GetByClientID(clientID) if err != nil { return nil, fmt.Errorf("failed to retrieve deployments for client %s: %w", clientID, err) } // Deserialize config fields for each deployment for _, deployment := range deployments { if err := s.deserializeConfigFields(deployment); err != nil { return nil, fmt.Errorf("failed to deserialize config fields: %w", err) } } return deployments, nil } // GetDeploymentsByUserID retrieves deployments created by a specific user func (s *DeploymentService) GetDeploymentsByUserID(userID string) ([]*models.Deployment, error) { deployments, err := s.store.GetByUserID(userID) if err != nil { return nil, fmt.Errorf("failed to retrieve deployments for user %s: %w", userID, err) } // Deserialize config fields for each deployment for _, deployment := range deployments { if err := s.deserializeConfigFields(deployment); err != nil { return nil, fmt.Errorf("failed to deserialize config fields: %w", err) } } return deployments, nil } // GetDeploymentsByBlueprintID retrieves deployments based on a specific blueprint func (s *DeploymentService) GetDeploymentsByBlueprintID(blueprintID string) ([]*models.Deployment, error) { // Check if blueprint exists blueprint, err := s.blueprintStore.GetByID(blueprintID) if err != nil { return nil, fmt.Errorf("failed to check if blueprint exists: %w", err) } if blueprint == nil { return nil, fmt.Errorf("blueprint with ID %s not found", blueprintID) } deployments, err := s.store.GetByBlueprintID(blueprintID) if err != nil { return nil, fmt.Errorf("failed to retrieve deployments for blueprint %s: %w", blueprintID, err) } // Deserialize config fields for each deployment for _, deployment := range deployments { if err := s.deserializeConfigFields(deployment); err != nil { return nil, fmt.Errorf("failed to deserialize config fields: %w", err) } } return deployments, nil } // UpdateDeploymentStatus updates the status of a deployment func (s *DeploymentService) UpdateDeploymentStatus(id string, status string) error { deployment, err := s.store.GetByID(id) if err != nil { return fmt.Errorf("failed to retrieve deployment: %w", err) } if deployment == nil { return fmt.Errorf("deployment with ID %s not found", id) } // Update the status deployment.Status = status // If status is being set to "deploying", update LastDeployedAt if status == string(models.DEPLOYING) { deployment.LastDeployedAt = time.Now() } if err := s.store.Update(deployment); err != nil { return fmt.Errorf("failed to update deployment status: %w", err) } return nil } // validateDeployment validates a deployment func (s *DeploymentService) validateDeployment(deployment *models.Deployment) error { // Validate required fields if deployment.Name == "" { return fmt.Errorf("deployment name is required") } // Validate relationships if deployment.ClientID == "" { return fmt.Errorf("client ID is required") } client, err := s.clientStore.GetByID(deployment.ClientID) if err != nil { return fmt.Errorf("failed to check client: %w", err) } if client == nil { return fmt.Errorf("client with ID %s not found", deployment.ClientID) } if deployment.BlueprintID == "" { return fmt.Errorf("blueprint ID is required") } blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID) if err != nil { return fmt.Errorf("failed to check blueprint: %w", err) } if blueprint == nil { return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID) } return nil } // setupDeployedComponents sets up deployed apps based on the blueprint func (s *DeploymentService) setupDeployedComponents(deployment *models.Deployment) error { // If deployment already has deployed apps defined, we assume they're set up correctly if len(deployment.DeployedComponents) > 0 { return nil } // Get the blueprint blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID) if err != nil { return fmt.Errorf("failed to retrieve blueprint: %w", err) } if blueprint == nil { return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID) } // Use the blueprint config to set up deployed apps var blueprintConfig models.BlueprintConfig if err := json.Unmarshal([]byte(blueprint.ConfigJSON), &blueprintConfig); err != nil { return fmt.Errorf("failed to parse blueprint config: %w", err) } // Create deployed apps for each component in the blueprint for _, componentConfig := range blueprintConfig.Components { // Get the component component, err := s.componentStore.GetByID(componentConfig.ID) if err != nil { return fmt.Errorf("failed to retrieve component: %w", err) } if component == nil { return fmt.Errorf("component with ID %s not found", componentConfig.ID) } // Create a deployed app deployedComponent := models.DeployedComponent{ ID: uuid.New().String(), DeploymentID: deployment.ID, ComponentID: component.ID, Status: string(models.PENDING_APP), Version: component.Version, URL: "", // Will be set during deployment PodCount: componentConfig.Autoscaling.MinReplicas, HealthStatus: string(models.HEALTHY), Resources: models.ResourceAllocation{ CPU: componentConfig.Resources.CPU, Memory: componentConfig.Resources.Memory, Storage: componentConfig.Resources.Storage, }, } // Add to deployment deployment.DeployedComponents = append(deployment.DeployedComponents, deployedComponent) } return nil } // processDeployment handles the actual deployment process func (s *DeploymentService) processDeployment(ctx context.Context, deployment *models.Deployment) { // Update status to deploying _ = s.UpdateDeploymentStatus(deployment.ID, string(models.DEPLOYING)) s.logger.WithField("deployment_id", deployment.ID).Info("Starting deployment process") // Check if we have a valid instance if deployment.InstanceID == "" { s.logger.WithField("deployment_id", deployment.ID).Error("No instance ID provided for deployment") _ = s.UpdateDeploymentStatus(deployment.ID, string(models.FAILED_DEPLOYMENT)) return } // Setup the deployed apps (database setup, migrations, etc.) if err := s.setupDeployedComponents(deployment); err != nil { s.logger.WithError(err).WithField("deployment_id", deployment.ID).Error("Failed to setup deployed apps") _ = s.UpdateDeploymentStatus(deployment.ID, string(models.FAILED_DEPLOYMENT)) return } // Update the deployment with completed status deployment.Status = string(models.DEPLOYED) deployment.LastDeployedAt = time.Now() if err := s.store.Update(deployment); err != nil { s.logger.WithError(err).WithField("deployment_id", deployment.ID).Error("Failed to update deployment status") } s.logger.WithField("deployment_id", deployment.ID).Info("Deployment completed successfully") } // processDeploymentCleanup handles the cleanup process for deleted deployments func (s *DeploymentService) processDeploymentCleanup(deploymentID string) { // Get the deployment deployment, err := s.store.GetByID(deploymentID) if err != nil { s.logger.WithError(err).WithField("deployment_id", deploymentID).Error("Failed to get deployment for cleanup") return } if deployment == nil { s.logger.WithField("deployment_id", deploymentID).Error("Deployment not found for cleanup") return } s.logger.WithField("deployment_id", deploymentID).Info("Starting deployment cleanup process") ctx := context.Background() // Otherwise, deprovision the instance provider, ok := cloud.GetProvider(deployment.Provider) if !ok { s.logger.WithField("provider", deployment.Provider).Error("Provider not found for cleanup") } else { s.logger.WithField("instance_id", deployment.InstanceID).Info("Terminating instance") if err := provider.ResetInstance(ctx, deployment.InstanceID); err != nil { s.logger.WithError(err).WithField("instance_id", deployment.InstanceID). Error("Failed to terminate instance") } } // Delete the deployment from the database if err := s.store.Delete(deploymentID); err != nil { s.logger.WithError(err).WithField("deployment_id", deploymentID). Error("Failed to delete deployment record") } s.logger.WithField("deployment_id", deploymentID).Info("Deployment cleanup completed") } // deserializeConfigFields deserializes JSON config fields from strings func (s *DeploymentService) deserializeConfigFields(deployment *models.Deployment) error { // Deserialize logs config if deployment.LogsConfig != "" { var logsConfig models.LogConfiguration if err := json.Unmarshal([]byte(deployment.LogsConfig), &logsConfig); err != nil { return fmt.Errorf("failed to unmarshal logs config: %w", err) } // We could set this on the deployment if needed } // Deserialize metrics config if deployment.MetricsConfig != "" { var metricsConfig models.MetricsConfiguration if err := json.Unmarshal([]byte(deployment.MetricsConfig), &metricsConfig); err != nil { return fmt.Errorf("failed to unmarshal metrics config: %w", err) } // We could set this on the deployment if needed } // Deserialize alerts config if deployment.AlertsConfig != "" { var alertsConfig []models.AlertConfiguration if err := json.Unmarshal([]byte(deployment.AlertsConfig), &alertsConfig); err != nil { return fmt.Errorf("failed to unmarshal alerts config: %w", err) } // We could set this on the deployment if needed } return nil }