package dbstore import ( "encoding/json" "fmt" "git.linuxforward.com/byop/byop-engine/dbmanager" "git.linuxforward.com/byop/byop-engine/models" "github.com/google/uuid" "gorm.io/gorm" ) // DeploymentStore handles database operations for deployments type DeploymentStore struct { db *gorm.DB } // NewDeploymentStore creates a new DeploymentStore func NewDeploymentStore(dbManager dbmanager.DbManager) *DeploymentStore { return &DeploymentStore{ db: dbManager.GetDB(), } } // Create creates a new deployment func (ds *DeploymentStore) Create(deployment *models.Deployment) error { // Generate ID if not provided if deployment.ID == "" { deployment.ID = uuid.New().String() } // Ensure logs, metrics, and alerts config are properly JSON serialized if err := ds.serializeConfigFields(deployment); err != nil { return fmt.Errorf("failed to serialize config fields: %w", err) } // Create deployment in a transaction to handle deployed apps return ds.db.Transaction(func(tx *gorm.DB) error { // Create the deployment if err := tx.Create(deployment).Error; err != nil { return err } // Create any deployed apps in the same transaction for i := range deployment.DeployedApps { app := &deployment.DeployedApps[i] // Ensure each deployed app has an ID if app.ID == "" { app.ID = uuid.New().String() } // Set the deployment ID (ensure relationship is maintained) app.DeploymentID = deployment.ID // Create the deployed app if err := tx.Create(app).Error; err != nil { return err } // Handle resources if provided if app.Resources != (models.ResourceAllocation{}) { resource := models.DeployedAppResource{ ID: uuid.New().String(), DeployedAppID: app.ID, CPU: app.Resources.CPU, CPUUsage: app.Resources.CPUUsage, Memory: app.Resources.Memory, MemoryUsage: app.Resources.MemoryUsage, Storage: app.Resources.Storage, StorageUsage: app.Resources.StorageUsage, } if err := tx.Create(&resource).Error; err != nil { return err } } } return nil }) } // GetByID retrieves a deployment by ID func (ds *DeploymentStore) GetByID(id string) (*models.Deployment, error) { var deployment models.Deployment // Get deployment with all related deployed apps err := ds.db. Preload("DeployedApps"). First(&deployment, "id = ?", id).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil // No deployment found } return nil, fmt.Errorf("failed to get deployment: %w", err) } // Load resources for each deployed app for i, app := range deployment.DeployedApps { var resource models.DeployedAppResource if err := ds.db.Where("deployed_app_id = ?", app.ID).First(&resource).Error; err != nil { if err != gorm.ErrRecordNotFound { return nil, fmt.Errorf("failed to get resources for deployed app: %w", err) } } else { deployment.DeployedApps[i].Resources = models.ResourceAllocation{ CPU: resource.CPU, CPUUsage: resource.CPUUsage, Memory: resource.Memory, MemoryUsage: resource.MemoryUsage, Storage: resource.Storage, StorageUsage: resource.StorageUsage, } } } // Deserialize config fields if err := ds.deserializeConfigFields(&deployment); err != nil { return nil, fmt.Errorf("failed to deserialize config fields: %w", err) } return &deployment, nil } // Update updates an existing deployment func (ds *DeploymentStore) Update(deployment *models.Deployment) error { // Ensure logs, metrics, and alerts config are properly JSON serialized if err := ds.serializeConfigFields(deployment); err != nil { return fmt.Errorf("failed to serialize config fields: %w", err) } // Use transaction to handle deployment and deployed apps return ds.db.Transaction(func(tx *gorm.DB) error { // Update the deployment if err := tx.Save(deployment).Error; err != nil { return err } // Handle deployed apps - this is trickier as we need to compare with existing apps var existingApps []models.DeployedApp if err := tx.Where("deployment_id = ?", deployment.ID).Find(&existingApps).Error; err != nil { return err } // Create a map of existing app IDs for quick lookup existingAppMap := make(map[string]bool) for _, app := range existingApps { existingAppMap[app.ID] = true } // Process each app in the updated deployment for i := range deployment.DeployedApps { app := &deployment.DeployedApps[i] // If app has ID and exists, update it if app.ID != "" && existingAppMap[app.ID] { if err := tx.Save(app).Error; err != nil { return err } delete(existingAppMap, app.ID) } else { // New app, create it if app.ID == "" { app.ID = uuid.New().String() } app.DeploymentID = deployment.ID if err := tx.Create(app).Error; err != nil { return err } } // Handle resources if app.Resources != (models.ResourceAllocation{}) { var resource models.DeployedAppResource result := tx.Where("deployed_app_id = ?", app.ID).First(&resource) if result.Error != nil && result.Error != gorm.ErrRecordNotFound { return result.Error } if result.Error == gorm.ErrRecordNotFound { // Create new resource resource = models.DeployedAppResource{ ID: uuid.New().String(), DeployedAppID: app.ID, CPU: app.Resources.CPU, CPUUsage: app.Resources.CPUUsage, Memory: app.Resources.Memory, MemoryUsage: app.Resources.MemoryUsage, Storage: app.Resources.Storage, StorageUsage: app.Resources.StorageUsage, } if err := tx.Create(&resource).Error; err != nil { return err } } else { // Update existing resource resource.CPU = app.Resources.CPU resource.CPUUsage = app.Resources.CPUUsage resource.Memory = app.Resources.Memory resource.MemoryUsage = app.Resources.MemoryUsage resource.Storage = app.Resources.Storage resource.StorageUsage = app.Resources.StorageUsage if err := tx.Save(&resource).Error; err != nil { return err } } } } // Delete any apps that are no longer part of the deployment for appID := range existingAppMap { if err := tx.Delete(&models.DeployedApp{}, "id = ?", appID).Error; err != nil { return err } // Delete associated resources if err := tx.Delete(&models.DeployedAppResource{}, "deployed_app_id = ?", appID).Error; err != nil && err != gorm.ErrRecordNotFound { return err } } return nil }) } // Delete deletes a deployment by ID func (ds *DeploymentStore) Delete(id string) error { return ds.db.Transaction(func(tx *gorm.DB) error { // Delete associated DeployedAppResources var deployedApps []models.DeployedApp if err := tx.Where("deployment_id = ?", id).Find(&deployedApps).Error; err != nil { return err } for _, app := range deployedApps { if err := tx.Delete(&models.DeployedAppResource{}, "deployed_app_id = ?", app.ID).Error; err != nil && err != gorm.ErrRecordNotFound { return err } } // Delete deployed apps if err := tx.Delete(&models.DeployedApp{}, "deployment_id = ?", id).Error; err != nil && err != gorm.ErrRecordNotFound { return err } // Delete the deployment itself return tx.Delete(&models.Deployment{}, "id = ?", id).Error }) } // List retrieves all deployments with optional filtering func (ds *DeploymentStore) List(filter map[string]interface{}) ([]*models.Deployment, error) { var deployments []*models.Deployment // Build query from filters query := ds.db.Preload("DeployedApps") if filter != nil { for key, value := range filter { query = query.Where(key+" = ?", value) } } // Execute query if err := query.Find(&deployments).Error; err != nil { return nil, fmt.Errorf("failed to list deployments: %w", err) } // Load resources and deserialize config for each deployment for i, deployment := range deployments { // Load resources for each deployed app for j, app := range deployment.DeployedApps { var resource models.DeployedAppResource if err := ds.db.Where("deployed_app_id = ?", app.ID).First(&resource).Error; err != nil { if err != gorm.ErrRecordNotFound { return nil, fmt.Errorf("failed to get resources for deployed app: %w", err) } } else { deployments[i].DeployedApps[j].Resources = models.ResourceAllocation{ CPU: resource.CPU, CPUUsage: resource.CPUUsage, Memory: resource.Memory, MemoryUsage: resource.MemoryUsage, Storage: resource.Storage, StorageUsage: resource.StorageUsage, } } } // Deserialize config fields if err := ds.deserializeConfigFields(deployments[i]); err != nil { return nil, fmt.Errorf("failed to deserialize config fields: %w", err) } } return deployments, nil } // GetByClientID retrieves deployments for a specific client func (ds *DeploymentStore) GetByClientID(clientID string) ([]*models.Deployment, error) { return ds.List(map[string]interface{}{"client_id": clientID}) } // GetByUserID retrieves deployments created by a specific user func (ds *DeploymentStore) GetByUserID(userID string) ([]*models.Deployment, error) { return ds.List(map[string]interface{}{"created_by": userID}) } // GetByTemplateID retrieves deployments based on a specific template func (ds *DeploymentStore) GetByTemplateID(templateID string) ([]*models.Deployment, error) { return ds.List(map[string]interface{}{"template_id": templateID}) } // serializeConfigFields serializes JSON config fields to strings func (ds *DeploymentStore) serializeConfigFields(deployment *models.Deployment) error { // Serialize logs config if provided if deployment.LogsConfig == "" { logsConfig := models.LogConfiguration{ Enabled: true, RetentionDays: 7, } logsConfigBytes, err := json.Marshal(logsConfig) if err != nil { return err } deployment.LogsConfig = string(logsConfigBytes) } // Serialize metrics config if provided if deployment.MetricsConfig == "" { metricsConfig := models.MetricsConfiguration{ Enabled: true, RetentionDays: 30, } metricsConfigBytes, err := json.Marshal(metricsConfig) if err != nil { return err } deployment.MetricsConfig = string(metricsConfigBytes) } // Serialize alerts config if provided if deployment.AlertsConfig == "" { alertsConfig := []models.AlertConfiguration{} alertsConfigBytes, err := json.Marshal(alertsConfig) if err != nil { return err } deployment.AlertsConfig = string(alertsConfigBytes) } return nil } // deserializeConfigFields deserializes JSON config fields from strings func (ds *DeploymentStore) deserializeConfigFields(deployment *models.Deployment) error { // No need to deserialize in the store, as these fields are stored as strings // in the database and are deserialized as needed by the service layer return nil }