package services import ( "encoding/json" "fmt" "time" "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.LastDeployedAt = now // Handle deployed apps setup if err := s.setupDeployedApps(deployment); err != nil { return fmt.Errorf("failed to setup deployed apps: %w", err) } // Persist the deployment if err := s.store.Create(deployment); err != nil { return fmt.Errorf("failed to create deployment: %w", err) } // Trigger deployment process (this would normally be asynchronous) // This is a placeholder for your actual deployment logic go s.processDeployment(deployment.ID) 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.DeployedApps) > 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.setupDeployedApps(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 } // setupDeployedApps sets up deployed apps based on the template func (s *DeploymentService) setupDeployedApps(deployment *models.Deployment) error { // If deployment already has deployed apps defined, we assume they're set up correctly if len(deployment.DeployedApps) > 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) } 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, 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.DeployedApps = append(deployment.DeployedApps, deployedApp) } 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(deploymentID, 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)) } } // 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 _ = s.store.Delete(deploymentID) } // 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 }