mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
709 lines
21 KiB
Go
709 lines
21 KiB
Go
package api
|
|
|
|
import (
|
|
"containr/internal/database"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// PreviewEnvironment represents a preview environment
|
|
type PreviewEnvironment struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
ProjectID uuid.UUID `json:"project_id" db:"project_id"`
|
|
ServiceID uuid.UUID `json:"service_id" db:"service_id"`
|
|
BranchName string `json:"branch_name" db:"branch_name"`
|
|
PRNumber *int `json:"pr_number" db:"pr_number"`
|
|
Environment string `json:"environment" db:"environment"` // preview-{branch}-{timestamp}
|
|
Status string `json:"status" db:"status"` // building, running, failed, stopped, expired
|
|
URL string `json:"url" db:"url"`
|
|
ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
|
|
// Related data
|
|
Service *Service `json:"service,omitempty"`
|
|
DeploymentID *uuid.UUID `json:"deployment_id,omitempty"`
|
|
}
|
|
|
|
// CreatePreviewEnvironmentRequest represents a request to create a preview environment
|
|
type CreatePreviewEnvironmentRequest struct {
|
|
ProjectID uuid.UUID `json:"project_id"`
|
|
ServiceID uuid.UUID `json:"service_id" binding:"required"`
|
|
BranchName string `json:"branch_name" binding:"required"`
|
|
PRNumber *int `json:"pr_number"`
|
|
TTLHours int `json:"ttl_hours" binding:"min=1,max=168"` // 1 hour to 7 days
|
|
}
|
|
|
|
// UpdatePreviewEnvironmentRequest represents a request to update a preview environment
|
|
type UpdatePreviewEnvironmentRequest struct {
|
|
Status string `json:"status" binding:"omitempty,oneof=building running failed stopped expired"`
|
|
URL string `json:"url"`
|
|
ExpiresAt *time.Time `json:"expires_at"`
|
|
TTLHours int `json:"ttl_hours" binding:"omitempty,min=1,max=168"`
|
|
}
|
|
|
|
// PromotePreviewEnvironmentRequest represents a request to promote a preview environment
|
|
type PromotePreviewEnvironmentRequest struct {
|
|
TargetEnvironment string `json:"target_environment" binding:"required,oneof=production development"`
|
|
CreateBackup bool `json:"create_backup"`
|
|
}
|
|
|
|
// handleGetPreviewEnvironments retrieves all preview environments for a project
|
|
func handleGetPreviewEnvironments(c *gin.Context) {
|
|
db, exists := c.Get("db")
|
|
if !exists {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
|
return
|
|
}
|
|
|
|
projectIDStr := firstPathParam(c, "id", "project_id", "projectId")
|
|
projectID, err := uuid.Parse(projectIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
|
return
|
|
}
|
|
|
|
// Check if project exists and user has access
|
|
var project Project
|
|
err = db.(*database.DB).QueryRow(
|
|
"SELECT id, name, owner_id FROM projects WHERE id = $1",
|
|
projectID,
|
|
).Scan(&project.ID, &project.Name, &project.OwnerID)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
|
return
|
|
}
|
|
|
|
// Get user ID from JWT token (set by auth middleware)
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Check if user owns the project
|
|
if project.OwnerID != userID.(string) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
|
return
|
|
}
|
|
|
|
// Get preview environments for the project with service info
|
|
rows, err := db.(*database.DB).Query(
|
|
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.pr_number,
|
|
pe.environment, pe.status, pe.url, pe.expires_at, pe.created_at, pe.updated_at,
|
|
s.id as service_id, s.name as service_name, s.type as service_type
|
|
FROM preview_environments pe
|
|
LEFT JOIN services s ON pe.service_id = s.id
|
|
WHERE pe.project_id = $1
|
|
ORDER BY pe.created_at DESC`,
|
|
projectID,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve preview environments"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var environments []PreviewEnvironment
|
|
for rows.Next() {
|
|
var env PreviewEnvironment
|
|
var serviceID sql.NullString
|
|
var serviceName sql.NullString
|
|
var serviceType sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&env.ID, &env.ProjectID, &env.ServiceID, &env.BranchName, &env.PRNumber,
|
|
&env.Environment, &env.Status, &env.URL, &env.ExpiresAt, &env.CreatedAt, &env.UpdatedAt,
|
|
&serviceID, &serviceName, &serviceType,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan preview environment"})
|
|
return
|
|
}
|
|
|
|
if serviceID.Valid {
|
|
parsedServiceID, parseErr := uuid.Parse(serviceID.String)
|
|
if parseErr == nil {
|
|
env.Service = &Service{
|
|
ID: parsedServiceID,
|
|
Name: serviceName.String,
|
|
Type: serviceType.String,
|
|
}
|
|
}
|
|
}
|
|
|
|
environments = append(environments, env)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"preview_environments": environments})
|
|
}
|
|
|
|
// handleCreatePreviewEnvironment creates a new preview environment
|
|
func handleCreatePreviewEnvironment(c *gin.Context) {
|
|
db, exists := c.Get("db")
|
|
if !exists {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
|
return
|
|
}
|
|
|
|
projectIDStr := firstPathParam(c, "id", "project_id", "projectId")
|
|
projectID, err := uuid.Parse(projectIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
|
return
|
|
}
|
|
|
|
var req CreatePreviewEnvironmentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.ProjectID == uuid.Nil {
|
|
req.ProjectID = projectID
|
|
} else if req.ProjectID != projectID {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Project ID in URL and request body must match"})
|
|
return
|
|
}
|
|
|
|
// Get user ID from JWT token
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Check if project exists and user has access
|
|
var project Project
|
|
err = db.(*database.DB).QueryRow(
|
|
"SELECT id, name, owner_id FROM projects WHERE id = $1",
|
|
req.ProjectID,
|
|
).Scan(&project.ID, &project.Name, &project.OwnerID)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
|
return
|
|
}
|
|
|
|
// Check if user owns the project
|
|
if project.OwnerID != userID.(string) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
|
return
|
|
}
|
|
|
|
// Check if service exists and belongs to the project
|
|
var service Service
|
|
err = db.(*database.DB).QueryRow(
|
|
"SELECT id, name, type FROM services WHERE id = $1 AND project_id = $2",
|
|
req.ServiceID, req.ProjectID,
|
|
).Scan(&service.ID, &service.Name, &service.Type)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found or doesn't belong to this project"})
|
|
return
|
|
}
|
|
|
|
// Check if preview environment already exists for this branch and service
|
|
var count int
|
|
err = db.(*database.DB).QueryRow(
|
|
"SELECT COUNT(*) FROM preview_environments WHERE service_id = $1 AND branch_name = $2 AND status NOT IN ('expired', 'stopped')",
|
|
req.ServiceID, req.BranchName,
|
|
).Scan(&count)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing preview environment"})
|
|
return
|
|
}
|
|
|
|
if count > 0 {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Preview environment already exists for this branch and service"})
|
|
return
|
|
}
|
|
|
|
// Set default TTL if not provided
|
|
ttlHours := req.TTLHours
|
|
if ttlHours == 0 {
|
|
ttlHours = 24 // Default 24 hours
|
|
}
|
|
|
|
// Create preview environment
|
|
env := PreviewEnvironment{
|
|
ID: uuid.New(),
|
|
ProjectID: req.ProjectID,
|
|
ServiceID: req.ServiceID,
|
|
BranchName: req.BranchName,
|
|
PRNumber: req.PRNumber,
|
|
Environment: generatePreviewEnvironmentName(req.BranchName),
|
|
Status: "building",
|
|
ExpiresAt: &[]time.Time{time.Now().Add(time.Duration(ttlHours) * time.Hour)}[0],
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
// Insert preview environment into database
|
|
_, err = db.(*database.DB).Exec(
|
|
`INSERT INTO preview_environments
|
|
(id, project_id, service_id, branch_name, pr_number, environment,
|
|
status, url, expires_at, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
|
env.ID, env.ProjectID, env.ServiceID, env.BranchName, env.PRNumber,
|
|
env.Environment, env.Status, env.URL, env.ExpiresAt, env.CreatedAt, env.UpdatedAt,
|
|
)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create preview environment"})
|
|
return
|
|
}
|
|
|
|
deploymentID := uuid.New()
|
|
sanitizedBranch := strings.ReplaceAll(req.BranchName, "/", "-")
|
|
sanitizedBranch = strings.ReplaceAll(sanitizedBranch, "_", "-")
|
|
version := fmt.Sprintf("preview-%s-%d", sanitizedBranch, time.Now().Unix())
|
|
startedAt := time.Now().UTC()
|
|
completedAt := startedAt
|
|
|
|
_, deployErr := db.(*database.DB).Exec(
|
|
`INSERT INTO deployments
|
|
(id, service_id, version, commit_hash, status, started_at, completed_at, deployment_log, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())`,
|
|
deploymentID,
|
|
req.ServiceID,
|
|
version,
|
|
req.BranchName,
|
|
"running",
|
|
startedAt,
|
|
completedAt,
|
|
fmt.Sprintf("Preview environment %s activated for branch %s", env.Environment, req.BranchName),
|
|
)
|
|
if deployErr != nil {
|
|
_, _ = db.(*database.DB).Exec(
|
|
`UPDATE preview_environments
|
|
SET status = 'failed', updated_at = NOW()
|
|
WHERE id = $1`,
|
|
env.ID,
|
|
)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to provision preview deployment"})
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(env.URL) == "" {
|
|
env.URL = fmt.Sprintf("https://%s.preview.containr.local", env.Environment)
|
|
}
|
|
env.Status = "running"
|
|
env.UpdatedAt = time.Now().UTC()
|
|
env.DeploymentID = &deploymentID
|
|
|
|
_, err = db.(*database.DB).Exec(
|
|
`UPDATE preview_environments
|
|
SET status = $1, url = $2, updated_at = $3
|
|
WHERE id = $4`,
|
|
env.Status,
|
|
env.URL,
|
|
env.UpdatedAt,
|
|
env.ID,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to finalize preview environment"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"preview_environment": env})
|
|
}
|
|
|
|
// handleGetPreviewEnvironment retrieves a specific preview environment
|
|
func handleGetPreviewEnvironment(c *gin.Context) {
|
|
db, exists := c.Get("db")
|
|
if !exists {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
|
return
|
|
}
|
|
|
|
envIDStr := c.Param("id")
|
|
envID, err := uuid.Parse(envIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preview environment ID"})
|
|
return
|
|
}
|
|
|
|
// Get user ID from JWT token
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Get preview environment with project ownership check
|
|
var env PreviewEnvironment
|
|
var serviceID sql.NullString
|
|
var serviceName sql.NullString
|
|
var serviceType sql.NullString
|
|
err = db.(*database.DB).QueryRow(
|
|
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.pr_number,
|
|
pe.environment, pe.status, pe.url, pe.expires_at, pe.created_at, pe.updated_at,
|
|
s.id as service_id, s.name as service_name, s.type as service_type
|
|
FROM preview_environments pe
|
|
LEFT JOIN services s ON pe.service_id = s.id
|
|
JOIN projects p ON pe.project_id = p.id
|
|
WHERE pe.id = $1 AND p.owner_id = $2`,
|
|
envID, userID,
|
|
).Scan(
|
|
&env.ID, &env.ProjectID, &env.ServiceID, &env.BranchName, &env.PRNumber,
|
|
&env.Environment, &env.Status, &env.URL, &env.ExpiresAt, &env.CreatedAt, &env.UpdatedAt,
|
|
&serviceID, &serviceName, &serviceType,
|
|
)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
|
|
return
|
|
}
|
|
|
|
// Populate service info if available
|
|
if serviceID.Valid {
|
|
parsedServiceID, parseErr := uuid.Parse(serviceID.String)
|
|
if parseErr == nil {
|
|
env.Service = &Service{
|
|
ID: parsedServiceID,
|
|
Name: serviceName.String,
|
|
Type: serviceType.String,
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"preview_environment": env})
|
|
}
|
|
|
|
// handleUpdatePreviewEnvironment updates a preview environment
|
|
func handleUpdatePreviewEnvironment(c *gin.Context) {
|
|
db, exists := c.Get("db")
|
|
if !exists {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
|
return
|
|
}
|
|
|
|
envIDStr := c.Param("id")
|
|
envID, err := uuid.Parse(envIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preview environment ID"})
|
|
return
|
|
}
|
|
|
|
var req UpdatePreviewEnvironmentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get user ID from JWT token
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Check if preview environment exists and user has access
|
|
var existingEnv PreviewEnvironment
|
|
err = db.(*database.DB).QueryRow(
|
|
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.pr_number,
|
|
pe.environment, pe.status, pe.url, pe.expires_at, pe.created_at, pe.updated_at
|
|
FROM preview_environments pe
|
|
JOIN projects p ON pe.project_id = p.id
|
|
WHERE pe.id = $1 AND p.owner_id = $2`,
|
|
envID, userID,
|
|
).Scan(
|
|
&existingEnv.ID, &existingEnv.ProjectID, &existingEnv.ServiceID, &existingEnv.BranchName,
|
|
&existingEnv.PRNumber, &existingEnv.Environment, &existingEnv.Status, &existingEnv.URL,
|
|
&existingEnv.ExpiresAt, &existingEnv.CreatedAt, &existingEnv.UpdatedAt,
|
|
)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
|
|
return
|
|
}
|
|
|
|
// Update fields if provided
|
|
if req.Status != "" {
|
|
existingEnv.Status = req.Status
|
|
}
|
|
if req.URL != "" {
|
|
existingEnv.URL = req.URL
|
|
}
|
|
if req.ExpiresAt != nil {
|
|
existingEnv.ExpiresAt = req.ExpiresAt
|
|
}
|
|
if req.TTLHours > 0 {
|
|
newExpiresAt := time.Now().Add(time.Duration(req.TTLHours) * time.Hour)
|
|
existingEnv.ExpiresAt = &newExpiresAt
|
|
}
|
|
|
|
existingEnv.UpdatedAt = time.Now()
|
|
|
|
// Update preview environment in database
|
|
_, err = db.(*database.DB).Exec(
|
|
`UPDATE preview_environments
|
|
SET status = $1, url = $2, expires_at = $3, updated_at = $4
|
|
WHERE id = $5`,
|
|
existingEnv.Status, existingEnv.URL, existingEnv.ExpiresAt, existingEnv.UpdatedAt, existingEnv.ID,
|
|
)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preview environment"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"preview_environment": existingEnv})
|
|
}
|
|
|
|
// handleDeletePreviewEnvironment deletes a preview environment
|
|
func handleDeletePreviewEnvironment(c *gin.Context) {
|
|
db, exists := c.Get("db")
|
|
if !exists {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
|
return
|
|
}
|
|
|
|
envIDStr := c.Param("id")
|
|
envID, err := uuid.Parse(envIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preview environment ID"})
|
|
return
|
|
}
|
|
|
|
// Get user ID from JWT token
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Check if preview environment exists and user has access
|
|
var projectOwnerID string
|
|
err = db.(*database.DB).QueryRow(
|
|
`SELECT p.owner_id
|
|
FROM preview_environments pe
|
|
JOIN projects p ON pe.project_id = p.id
|
|
WHERE pe.id = $1`,
|
|
envID,
|
|
).Scan(&projectOwnerID)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
|
|
return
|
|
}
|
|
|
|
// Check if user owns the project
|
|
if projectOwnerID != userID.(string) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
|
return
|
|
}
|
|
|
|
cleanupID := uuid.New()
|
|
cleanupStartedAt := time.Now().UTC()
|
|
cleanupCompletedAt := cleanupStartedAt
|
|
|
|
_, _ = db.(*database.DB).Exec(
|
|
`INSERT INTO deployments
|
|
(id, service_id, version, status, started_at, completed_at, deployment_log, created_at, updated_at)
|
|
VALUES ($1, (SELECT service_id FROM preview_environments WHERE id = $2), $3, $4, $5, $6, $7, NOW(), NOW())`,
|
|
cleanupID,
|
|
envID,
|
|
"preview-cleanup",
|
|
"rolled_back",
|
|
cleanupStartedAt,
|
|
cleanupCompletedAt,
|
|
"Preview environment resources cleaned up",
|
|
)
|
|
|
|
// Delete preview environment
|
|
_, err = db.(*database.DB).Exec(
|
|
"DELETE FROM preview_environments WHERE id = $1",
|
|
envID,
|
|
)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete preview environment"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Preview environment deleted successfully"})
|
|
}
|
|
|
|
// handlePromotePreviewEnvironment promotes a preview environment to production/development
|
|
func handlePromotePreviewEnvironment(c *gin.Context) {
|
|
db, exists := c.Get("db")
|
|
if !exists {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
|
return
|
|
}
|
|
|
|
envIDStr := c.Param("id")
|
|
envID, err := uuid.Parse(envIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preview environment ID"})
|
|
return
|
|
}
|
|
|
|
var req PromotePreviewEnvironmentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get user ID from JWT token
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Get preview environment details
|
|
var env PreviewEnvironment
|
|
err = db.(*database.DB).QueryRow(
|
|
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.environment, pe.status
|
|
FROM preview_environments pe
|
|
JOIN projects p ON pe.project_id = p.id
|
|
WHERE pe.id = $1 AND p.owner_id = $2`,
|
|
envID, userID,
|
|
).Scan(
|
|
&env.ID, &env.ProjectID, &env.ServiceID, &env.BranchName, &env.Environment, &env.Status,
|
|
)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
|
|
return
|
|
}
|
|
|
|
// Check if preview environment is in a state that can be promoted
|
|
if env.Status != "running" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Preview environment must be running to promote"})
|
|
return
|
|
}
|
|
|
|
deploymentID := uuid.New()
|
|
_, err = db.(*database.DB).Exec(
|
|
`INSERT INTO deployments (id, service_id, status, created_at, updated_at)
|
|
VALUES ($1, $2, $3, NOW(), NOW())`,
|
|
deploymentID, env.ServiceID, "pending",
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create promotion deployment"})
|
|
return
|
|
}
|
|
|
|
if _, err := db.(*database.DB).Exec(
|
|
`UPDATE services
|
|
SET environment = $1, updated_at = NOW()
|
|
WHERE id = $2`,
|
|
req.TargetEnvironment, env.ServiceID,
|
|
); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service target environment"})
|
|
return
|
|
}
|
|
|
|
previewStatus := "stopped"
|
|
if req.CreateBackup {
|
|
previewStatus = "expired"
|
|
}
|
|
|
|
if _, err := db.(*database.DB).Exec(
|
|
`UPDATE preview_environments
|
|
SET status = $1, updated_at = NOW()
|
|
WHERE id = $2`,
|
|
previewStatus, env.ID,
|
|
); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preview environment status"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Preview environment promoted successfully",
|
|
"promotion": map[string]interface{}{
|
|
"preview_environment_id": env.ID,
|
|
"target_environment": req.TargetEnvironment,
|
|
"branch_name": env.BranchName,
|
|
"create_backup": req.CreateBackup,
|
|
"deployment_id": deploymentID,
|
|
"status": "queued",
|
|
"preview_status": previewStatus,
|
|
},
|
|
})
|
|
}
|
|
|
|
// generatePreviewEnvironmentName generates a unique environment name for preview
|
|
func generatePreviewEnvironmentName(branchName string) string {
|
|
timestamp := time.Now().Format("20060102-150405")
|
|
// Sanitize branch name
|
|
sanitizedBranch := strings.ReplaceAll(branchName, "/", "-")
|
|
sanitizedBranch = strings.ReplaceAll(sanitizedBranch, "_", "-")
|
|
return fmt.Sprintf("preview-%s-%s", sanitizedBranch, timestamp)
|
|
}
|
|
|
|
// handleCleanupExpiredPreviewEnvironments cleans up expired preview environments
|
|
func handleCleanupExpiredPreviewEnvironments(c *gin.Context) {
|
|
db, exists := c.Get("db")
|
|
if !exists {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
|
return
|
|
}
|
|
|
|
// Get user ID from JWT token
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Find expired preview environments for user's projects
|
|
rows, err := db.(*database.DB).Query(
|
|
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.environment
|
|
FROM preview_environments pe
|
|
JOIN projects p ON pe.project_id = p.id
|
|
WHERE p.owner_id = $1 AND pe.expires_at < NOW() AND pe.status != 'expired'`,
|
|
userID,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to find expired preview environments"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var expiredEnvs []PreviewEnvironment
|
|
for rows.Next() {
|
|
var env PreviewEnvironment
|
|
err := rows.Scan(
|
|
&env.ID, &env.ProjectID, &env.ServiceID, &env.BranchName, &env.Environment,
|
|
)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
expiredEnvs = append(expiredEnvs, env)
|
|
}
|
|
|
|
// Mark expired environments as expired and trigger cleanup
|
|
cleanupCount := 0
|
|
for _, env := range expiredEnvs {
|
|
// Update status to expired
|
|
_, err := db.(*database.DB).Exec(
|
|
"UPDATE preview_environments SET status = 'expired', updated_at = NOW() WHERE id = $1",
|
|
env.ID,
|
|
)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
cleanupCount++
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Cleanup completed",
|
|
"cleaned_count": cleanupCount,
|
|
"expired_environments": expiredEnvs,
|
|
})
|
|
}
|