diff --git a/apps/api-server/internal/services/deployment.go b/apps/api-server/internal/services/deployment.go new file mode 100644 index 0000000..082f160 --- /dev/null +++ b/apps/api-server/internal/services/deployment.go @@ -0,0 +1,157 @@ +package services + +import ( + "context" + "errors" + "fmt" + "time" + + "gorm.io/gorm" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/database" + "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" +) + +// DeploymentService persists deployment records. Actual orchestration +// (Helm/ArgoCD/kubectl) is handled by an external controller or operator; +// this service is the system of record for deployment history and status. +type DeploymentService struct { + db *database.Database +} + +func NewDeploymentService(db *database.Database) *DeploymentService { + return &DeploymentService{db: db} +} + +type DeploymentFilter struct { + Service string + Environment string + Status string + Limit int + Offset int +} + +func (s *DeploymentService) List(ctx context.Context, f DeploymentFilter) ([]models.DeploymentRecord, int64, error) { + q := s.db.DB.WithContext(ctx).Model(&models.DeploymentRecord{}) + if f.Service != "" { + q = q.Where("service = ?", f.Service) + } + if f.Environment != "" { + q = q.Where("environment = ?", f.Environment) + } + if f.Status != "" { + q = q.Where("status = ?", f.Status) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + if f.Limit <= 0 { + f.Limit = 20 + } + var out []models.DeploymentRecord + if err := q.Order("created_at DESC").Limit(f.Limit).Offset(f.Offset).Find(&out).Error; err != nil { + return nil, 0, err + } + return out, total, nil +} + +func (s *DeploymentService) Get(ctx context.Context, id uint) (*models.DeploymentRecord, error) { + var d models.DeploymentRecord + if err := s.db.DB.WithContext(ctx).First(&d, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("deployment %d not found", id) + } + return nil, err + } + return &d, nil +} + +type StartDeploymentInput struct { + Service string `json:"service" binding:"required"` + Version string `json:"version" binding:"required"` + Environment string `json:"environment" binding:"required"` + Strategy string `json:"strategy"` + Replicas int `json:"replicas"` + TriggeredBy string `json:"triggered_by"` + Reason string `json:"reason"` +} + +func (s *DeploymentService) Start(ctx context.Context, in StartDeploymentInput) (*models.DeploymentRecord, error) { + now := time.Now() + d := models.DeploymentRecord{ + Service: in.Service, + Version: in.Version, + Environment: in.Environment, + Strategy: defaultStr(in.Strategy, "rolling"), + Replicas: in.Replicas, + TriggeredBy: defaultStr(in.TriggeredBy, "api"), + Reason: in.Reason, + Status: "in_progress", + StartedAt: &now, + } + if err := s.db.DB.WithContext(ctx).Create(&d).Error; err != nil { + return nil, err + } + return &d, nil +} + +type RollbackInput struct { + Service string `json:"service" binding:"required"` + ToVersion string `json:"to_version" binding:"required"` + Reason string `json:"reason"` + TriggeredBy string `json:"triggered_by"` +} + +func (s *DeploymentService) Rollback(ctx context.Context, in RollbackInput) (*models.DeploymentRecord, error) { + // Ensure the target version exists in history for that service. + var prior models.DeploymentRecord + err := s.db.DB.WithContext(ctx). + Where("service = ? AND version = ?", in.Service, in.ToVersion). + Order("created_at DESC"). + First(&prior).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("no prior deployment of %s at version %s", in.Service, in.ToVersion) + } + return nil, err + } + + now := time.Now() + d := models.DeploymentRecord{ + Service: in.Service, + Version: in.ToVersion, + Environment: prior.Environment, + Strategy: "rolling", + Replicas: prior.Replicas, + TriggeredBy: defaultStr(in.TriggeredBy, "api"), + Reason: in.Reason, + Status: "rolled_back", + StartedAt: &now, + RollbackTo: in.ToVersion, + } + if err := s.db.DB.WithContext(ctx).Create(&d).Error; err != nil { + return nil, err + } + return &d, nil +} + +// MarkCompleted updates deployment status; intended to be called by the +// orchestration layer (controller) once the real rollout finishes. +func (s *DeploymentService) MarkCompleted(ctx context.Context, id uint, success bool) (*models.DeploymentRecord, error) { + d, err := s.Get(ctx, id) + if err != nil { + return nil, err + } + now := time.Now() + d.CompletedAt = &now + if success { + d.Status = "completed" + } else { + d.Status = "failed" + } + if err := s.db.DB.WithContext(ctx).Save(d).Error; err != nil { + return nil, err + } + return d, nil +}