Files
nutcas3 14ca3d00e7 feat: Add deployment service with CRUD operations, rollback support, and status tracking
- Add DeploymentService struct with database dependency and NewDeploymentService constructor
- Add DeploymentFilter struct with Service/Environment/Status/Limit/Offset fields for query filtering
- Add List method with context support, filter application (service/environment/status), pagination (default limit: 20), and DESC ordering by created_at
- Add Get method with gorm.ErrRecordNotFound handling and formatted
2026-04-20 20:06:09 +03:00

158 lines
4.4 KiB
Go

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
}