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" ) // DeploymentService handles business logic for deployments type DeploymentService struct { store *dbstore.DeploymentStore componentStore *dbstore.ComponentStore // Renamed from appStore appStore *dbstore.AppStore // Renamed from templateStore clientStore *dbstore.ClientStore } // NewDeploymentService creates a new DeploymentService func NewDeploymentService( store *dbstore.DeploymentStore, componentStore *dbstore.ComponentStore, appStore *dbstore.AppStore, clientStore *dbstore.ClientStore, ) *DeploymentService { return &DeploymentService{ store: store, componentStore: componentStore, appStore: appStore, clientStore: clientStore, } } // CreateDeployment creates a new deployment func (s *DeploymentService) CreateDeployment(deployment *models.Deployment) error { // 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 int64) (*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 == 0 { 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 %d 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 int64) 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 %d 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 int64) ([]*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 %d 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 } // GetDeploymentsByAppID retrieves deployments based on a specific app (was template) func (s *DeploymentService) GetDeploymentsByAppID(appID int64) ([]*models.Deployment, error) { // Check if app exists app, err := s.appStore.GetByID(appID) if err != nil { return nil, fmt.Errorf("failed to check if app exists: %w", err) } if app == nil { return nil, fmt.Errorf("app with ID %d not found", appID) } deployments, err := s.store.GetByAppID(appID) if err != nil { return nil, fmt.Errorf("failed to retrieve deployments for app %s: %w", appID, 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 int64, 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 == 0 { 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 %d not found", deployment.ClientID) } if deployment.AppID == 0 { return fmt.Errorf("app ID is required") } app, err := s.appStore.GetByID(deployment.AppID) if err != nil { return fmt.Errorf("failed to check app: %w", err) } if app == nil { return fmt.Errorf("app with ID %s not found", deployment.AppID) } 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 app app, err := s.appStore.GetByID(deployment.AppID) if err != nil { return fmt.Errorf("failed to retrieve app: %w", err) } if app == nil { return fmt.Errorf("app with ID %d not found", deployment.AppID) } // Use the app config to set up deployed apps var appConfig models.AppConfig if err := json.Unmarshal([]byte(app.ConfigJSON), &appConfig); err != nil { return fmt.Errorf("failed to parse app config: %w", err) } // Create deployed apps for each component in the app for _, componentConfig := range appConfig.Components { // Get the component component, err := s.componentStore.GetByID(componentConfig.ID) if err != nil { return fmt.Errorf("failed to retrieve component: %w", err) return fmt.Errorf("failed to retrieve component: %w", err) } if component == nil { return fmt.Errorf("component with ID %d not found", componentConfig.ID) } // Create a deployed app (GORM will auto-generate ID) deployedApp := models.DeployedApp{ DeploymentID: deployment.ID, ComponentID: component.ID, ComponentID: component.ID, Status: string(models.PENDING_APP), Version: component.Version, Version: component.Version, URL: "", // Will be set during deployment PodCount: componentConfig.Autoscaling.MinReplicas, PodCount: componentConfig.Autoscaling.MinReplicas, HealthStatus: string(models.HEALTHY), Resources: models.ResourceAllocation{ CPU: componentConfig.Resources.CPU, Memory: componentConfig.Resources.Memory, Storage: componentConfig.Resources.Storage, 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(deploymentID int64) { // This would be an async process in a real system // For now, we just update the status after a short delay to simulate the process // Update status to deploying _ = s.UpdateDeploymentStatus(deployment.ID, string(models.DEPLOYING)) // In a real system, this would be where you'd: // 1. Provision infrastructure // 2. Deploy containers/apps // 3. Configure networking // 4. Setup monitoring // etc. // Logging the deployment process fmt.Printf("Processing deployment %d...\n", deploymentID) for i := 0; i < 5; i++ { fmt.Printf("Deploying app %d/%d...\n", i+1, 5) time.Sleep(500 * time.Millisecond) // Simulate work } // For this demo, we'll just update the status after a short delay time.Sleep(2 * time.Second) // Update status to deployed or failed (randomly for demonstration) if time.Now().Unix()%2 == 0 { // Random success/failure _ = s.UpdateDeploymentStatus(deploymentID, string(models.DEPLOYED)) } else { _ = s.UpdateDeploymentStatus(deploymentID, string(models.FAILED_DEPLOYMENT)) } s.logger.WithField("deployment_id", deployment.ID).Info("Deployment completed successfully") } // processDeploymentCleanup handles the cleanup process for deleted deployments func (s *DeploymentService) processDeploymentCleanup(deploymentID int64) { // This would be an async process in a real system // In a real system, this would: // 1. Deprovision infrastructure // 2. Clean up resources // 3. Remove configuration // For this demo, we'll just delete after a short delay time.Sleep(2 * time.Second) // 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 }