Compare commits

...
Author SHA1 Message Date
Tomas DvorakandDevin 19e096f8ae refactor(db): port deployments to sqlc
Tranche 4: all 32 raw sites in deployments.go — paginated service list,
the accessible-feed joins (owner/member/admin/approved), service+owner
load, deployment insert, status transitions (fail/complete/set), the
10-field progress sync, cancel access check, rollback reads/inserts, and
the published_port reuse read. The shared serviceForDeployFromRow mapper
now feeds both the preview and deployments loaders. Live test covers the
full lifecycle: insert, list, access join, feed predicate, queued →
deployed sync, failed transition.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-10 03:52:36 +02:00
Tomáš DvořákandDevin 202fdd7b5a refactor(db): port preview environments to sqlc (#77)
CI / Changed paths (push) Canceled after 0s
CI / Infra (Compose + Scripts) (push) Canceled after 0s
CI / Backend (Go) (push) Canceled after 0s
CI / Frontend (React) (push) Canceled after 0s
CI / Mobile (Expo) (push) Canceled after 0s
CI / Compose boot smoke (push) Canceled after 0s
CI / Build & publish images (push) Canceled after 0s
CI / Release (push) Canceled after 0s
Tranche 3: all 30 raw sites in preview_environments.go — list/detail
joins, access-check reads, create/update/delete, promote, user cleanup,
TTL sweep, and the status-sync UPDATE...FROM CASE. Deployments and
preview_environments schema.sql definitions completed to match runtime.
Shared mapper converts generated rows to the PreviewEnvironment DTO;
DeleteServiceByID from tranche 2 is reused for clone teardown.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-10 03:39:54 +02:00
Tomáš DvořákandDevin 687555fc78 refactor(db): port service clone/move ops to sqlc (#76)
Tranche 2: cloneServiceRow's INSERT...SELECT (volumes and traefik_labels
now read straight from the source row — traefik_labels was silently
dropped before, and the jsonb param round-trip goes away), domain and
variable copies, name-conflict counts, move UPDATE, project owner check.
Also fixes serviceWriteAccess: user_id is a context string, not a
uuid.UUID — the tranche-1 cast silently failed owner checks for
non-admins. Live tests cover the domain invariants and a full clone
round-trip (config, domains, variables).

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-10 03:25:07 +02:00
14 changed files with 2542 additions and 621 deletions

No files matched your search

+270 -278
View File
@@ -2,12 +2,14 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
"containr/internal/docker" "containr/internal/docker"
"containr/internal/source" "containr/internal/source"
"containr/internal/types" "containr/internal/types"
"context" "context"
"database/sql"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -37,6 +39,72 @@ type DeploymentModel struct {
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
} }
// deploymentRowShape carries the deployment columns shared by the
// generated list/detail/rollback row types.
type deploymentRowShape struct {
ID uuid.UUID
ServiceID uuid.UUID
CommitHash sql.NullString
Status sql.NullString
ImageName sql.NullString
ImageTag sql.NullString
BuildLog sql.NullString
RuntimeLog sql.NullString
Error sql.NullString
StartedAt sql.NullTime
CompletedAt sql.NullTime
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}
func strPtrNS(v sql.NullString) *string {
if !v.Valid {
return nil
}
s := v.String
return &s
}
func ptrStr(s *string) string {
if s == nil {
return ""
}
return *s
}
func timePtrNT(v sql.NullTime) *time.Time {
if !v.Valid {
return nil
}
t := v.Time
return &t
}
func ntPtr(t *time.Time) sql.NullTime {
if t == nil {
return sql.NullTime{}
}
return sql.NullTime{Time: *t, Valid: true}
}
func deploymentModelFrom(r deploymentRowShape) DeploymentModel {
return DeploymentModel{
ID: r.ID,
ServiceID: r.ServiceID,
CommitHash: strPtrNS(r.CommitHash),
Status: r.Status.String,
ImageName: r.ImageName.String,
ImageTag: r.ImageTag.String,
BuildLog: r.BuildLog.String,
RuntimeLog: r.RuntimeLog.String,
Error: strPtrNS(r.Error),
StartedAt: timePtrNT(r.StartedAt),
CompletedAt: timePtrNT(r.CompletedAt),
CreatedAt: r.CreatedAt.Time,
UpdatedAt: r.UpdatedAt.Time,
}
}
type CreateDeploymentRequest struct { type CreateDeploymentRequest struct {
CommitHash string `json:"commit_hash"` CommitHash string `json:"commit_hash"`
Branch string `json:"branch"` Branch string `json:"branch"`
@@ -84,44 +152,29 @@ func handleGetDeployments(c *gin.Context) {
offset, limit := pageWindow(c, 50, 100) offset, limit := pageWindow(c, 50, 100)
var total int q := sqlcdb.New(db.(*database.DB).DB)
_ = db.(*database.DB).QueryRow( total, _ := q.CountDeploymentsForService(c.Request.Context(), serviceID)
`SELECT COUNT(*) FROM deployments WHERE service_id = $1`, serviceID,
).Scan(&total)
rows, err := db.(*database.DB).Query( rows, err := q.ListDeploymentsForService(c.Request.Context(), sqlcdb.ListDeploymentsForServiceParams{
`SELECT id, service_id, commit_hash, status, image_name, image_tag, ServiceID: serviceID, Limit: int32(limit), Offset: int32(offset)})
build_log, runtime_log, error, started_at, completed_at, created_at, updated_at
FROM deployments
WHERE service_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`,
serviceID, limit, offset,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"})
return return
} }
defer rows.Close()
var deployments []DeploymentModel deployments := make([]DeploymentModel, 0, len(rows))
for rows.Next() { for _, r := range rows {
var d DeploymentModel deployments = append(deployments, deploymentModelFrom(deploymentRowShape{
err := rows.Scan( ID: r.ID, ServiceID: r.ServiceID, CommitHash: r.CommitHash, Status: r.Status,
&d.ID, &d.ServiceID, &d.CommitHash, &d.Status, &d.ImageName, &d.ImageTag, ImageName: r.ImageName, ImageTag: r.ImageTag, BuildLog: r.BuildLog,
&d.BuildLog, &d.RuntimeLog, &d.Error, &d.StartedAt, &d.CompletedAt, RuntimeLog: r.RuntimeLog, Error: r.Error, StartedAt: r.StartedAt,
&d.CreatedAt, &d.UpdatedAt, CompletedAt: r.CompletedAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
) }))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan deployment"})
return
}
deployments = append(deployments, d)
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"deployments": deployments, "deployments": deployments,
"pagination": paginationMeta(offset, limit, total), "pagination": paginationMeta(offset, limit, int(total)),
}) })
} }
@@ -152,51 +205,33 @@ func handleGetRecentDeployments(c *gin.Context) {
offset, limit := pageWindow(c, 10, 50) offset, limit := pageWindow(c, 10, 50)
var total int q := sqlcdb.New(db.(*database.DB).DB)
if err := db.(*database.DB).QueryRow( total, err := q.CountAccessibleDeployments(c.Request.Context(), sqlcdb.CountAccessibleDeploymentsParams{
`SELECT COUNT(*) OwnerID: userID, Column2: isAdmin})
FROM deployments d if err != nil {
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)`,
userID, isAdmin,
).Scan(&total); err != nil {
total = 0 total = 0
} }
rows, err := db.(*database.DB).Query( rows, err := q.ListRecentAccessibleDeployments(c.Request.Context(), sqlcdb.ListRecentAccessibleDeploymentsParams{
`SELECT d.id, d.service_id, s.name, p.name, d.status, OwnerID: userID, Column2: isAdmin, Limit: int32(limit), Offset: int32(offset)})
COALESCE(d.image_name, ''), d.started_at, d.completed_at, d.created_at
FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)
ORDER BY d.created_at DESC
LIMIT $3 OFFSET $4`,
userID, isAdmin, limit, offset,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"})
return return
} }
defer rows.Close()
deployments := []RecentDeployment{} deployments := make([]RecentDeployment, 0, len(rows))
for rows.Next() { for _, r := range rows {
var d RecentDeployment deployments = append(deployments, RecentDeployment{
if err := rows.Scan(&d.ID, &d.ServiceID, &d.ServiceName, &d.ProjectName, ID: r.ID, ServiceID: r.ServiceID, ServiceName: r.ServiceName,
&d.Status, &d.ImageName, &d.StartedAt, &d.CompletedAt, &d.CreatedAt); err != nil { ProjectName: r.ProjectName, Status: r.Status.String, ImageName: r.ImageName,
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan deployment"}) StartedAt: timePtrNT(r.StartedAt), CompletedAt: timePtrNT(r.CompletedAt),
return CreatedAt: r.CreatedAt.Time,
} })
deployments = append(deployments, d)
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"deployments": deployments, "deployments": deployments,
"pagination": paginationMeta(offset, limit, total), "pagination": paginationMeta(offset, limit, int(total)),
}) })
} }
@@ -230,38 +265,27 @@ func handleCreateDeployment(c *gin.Context) {
return return
} }
var service Service ctx := c.Request.Context()
var projectOwner string q := sqlcdb.New(db.(*database.DB).DB)
err = db.(*database.DB).QueryRow( sr, err := q.GetServiceForDeployWithOwner(ctx, serviceID)
`SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at, p.owner_id
FROM services s
JOIN projects p ON s.project_id = p.id
WHERE s.id = $1`,
serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt, &projectOwner,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return return
} }
service := serviceForDeployFromRow(sqlcdb.GetServiceForDeployRow{
ID: sr.ID, ProjectID: sr.ProjectID, Name: sr.Name, Type: sr.Type,
Status: sr.Status, Image: sr.Image, Command: sr.Command,
Environment: sr.Environment, GitRepo: sr.GitRepo, GitBranch: sr.GitBranch,
BuildPath: sr.BuildPath, Cpu: sr.Cpu, Memory: sr.Memory,
Replicas: sr.Replicas, Port: sr.Port, Domain: sr.Domain,
HealthcheckPath: sr.HealthcheckPath, RestartPolicy: sr.RestartPolicy,
Builder: sr.Builder, CpuReserve: sr.CpuReserve, MemoryReserve: sr.MemoryReserve,
StaticBuildCmd: sr.StaticBuildCmd, StaticDir: sr.StaticDir,
CreatedAt: sr.CreatedAt, UpdatedAt: sr.UpdatedAt,
})
projectOwner := sr.OwnerID
if projectOwner != userID.(string) { if projectOwner.String() != userID.(string) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -287,12 +311,17 @@ func handleCreateDeployment(c *gin.Context) {
UpdatedAt: now, UpdatedAt: now,
} }
_, err = db.(*database.DB).Exec( err = q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{
`INSERT INTO deployments ID: d.ID,
(id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at) ServiceID: d.ServiceID,
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, Version: fmt.Sprintf("v%d", now.Unix()),
d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt, CommitHash: sql.NullString{String: ptrStr(d.CommitHash), Valid: d.CommitHash != nil},
) Status: sql.NullString{String: d.Status, Valid: true},
ImageName: sql.NullString{String: d.ImageName, Valid: true},
ImageTag: sql.NullString{String: d.ImageTag, Valid: true},
CreatedAt: sql.NullTime{Time: d.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: d.UpdatedAt, Valid: true},
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create deployment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create deployment"})
@@ -303,20 +332,20 @@ func handleCreateDeployment(c *gin.Context) {
if !exists || engine == nil { if !exists || engine == nil {
unavailableErr := "Deployment engine unavailable. Docker may not be configured on this server." unavailableErr := "Deployment engine unavailable. Docker may not be configured on this server."
completedAt := time.Now() completedAt := time.Now()
_, _ = db.(*database.DB).Exec( _ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
`UPDATE deployments Error: sql.NullString{String: unavailableErr, Valid: true},
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 CompletedAt: sql.NullTime{Time: completedAt, Valid: true},
WHERE id = $3`, ID: d.ID,
unavailableErr, completedAt, d.ID, })
)
d.Status = "failed" d.Status = "failed"
d.Error = &unavailableErr d.Error = &unavailableErr
d.CompletedAt = &completedAt d.CompletedAt = &completedAt
} else { } else {
_, err = db.(*database.DB).Exec( err = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
`UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`, Status: sql.NullString{String: "building", Valid: true},
time.Now(), serviceID, UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
) ID: serviceID,
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service status"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service status"})
return return
@@ -331,14 +360,17 @@ func handleCreateDeployment(c *gin.Context) {
}) })
if pos > 0 { if pos > 0 {
d.Status = "queued" d.Status = "queued"
_, _ = db.(*database.DB).Exec( now := time.Now()
`UPDATE deployments SET status = 'queued', updated_at = $1 WHERE id = $2`, _ = q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{
time.Now(), d.ID, Status: sql.NullString{String: "queued", Valid: true},
) UpdatedAt: sql.NullTime{Time: now, Valid: true},
_, _ = db.(*database.DB).Exec( ID: d.ID,
`UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`, })
time.Now(), serviceID, _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
) Status: sql.NullString{String: "queued", Valid: true},
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: serviceID,
})
} }
} }
@@ -380,6 +412,7 @@ func runDeploymentAndSyncWithImage(
) { ) {
ctx, cancel := context.WithTimeout(parentCtx, 30*time.Minute) ctx, cancel := context.WithTimeout(parentCtx, 30*time.Minute)
defer cancel() defer cancel()
q := sqlcdb.New(db.DB)
// The checkout and any shipped artifacts die with this deployment. // The checkout and any shipped artifacts die with this deployment.
var artifactIDs []string var artifactIDs []string
@@ -392,10 +425,11 @@ func runDeploymentAndSyncWithImage(
}() }()
failDeploy := func(msg string) { failDeploy := func(msg string) {
failedAt := time.Now() failedAt := time.Now()
_, _ = db.Exec( _ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
`UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`, Error: sql.NullString{String: msg, Valid: true},
msg, failedAt, dbDeployment.ID, CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
) ID: dbDeployment.ID,
})
} }
sourcePath := strings.TrimSpace(service.BuildPath) sourcePath := strings.TrimSpace(service.BuildPath)
@@ -423,11 +457,10 @@ func runDeploymentAndSyncWithImage(
replicas = 1 replicas = 1
} }
var publishedPort int32
// Best effort: reuse the host port from the last live deployment so public // Best effort: reuse the host port from the last live deployment so public
// URLs survive redeploys. Column exists post-migration; older DBs get // URLs survive redeploys. Column exists post-migration; older DBs get
// ephemeral ports. // ephemeral ports.
_ = db.QueryRow(`SELECT published_port FROM services WHERE id = $1`, service.ID).Scan(&publishedPort) publishedPort, _ := q.GetServicePublishedPort(ctx, service.ID)
publicPort := int32(service.Port) publicPort := int32(service.Port)
if service.Builder == "static" && publicPort == 0 { if service.Builder == "static" && publicPort == 0 {
@@ -451,7 +484,9 @@ func runDeploymentAndSyncWithImage(
// remote builds package it into a context artifact the node builds. // remote builds package it into a context artifact the node builds.
if service.GitRepo != "" && imageOverride == "" { if service.GitRepo != "" && imageOverride == "" {
var ownerID string var ownerID string
_ = db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, service.ProjectID).Scan(&ownerID) if o, err := q.GetProjectOwner(ctx, service.ProjectID); err == nil {
ownerID = o.String()
}
branch := req.Branch branch := req.Branch
if branch == "" { if branch == "" {
branch = service.GitBranch branch = service.GitBranch
@@ -535,7 +570,9 @@ func runDeploymentAndSyncWithImage(
} }
} else { } else {
var ownerID string var ownerID string
_ = db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, service.ProjectID).Scan(&ownerID) if o, err := q.GetProjectOwner(ctx, service.ProjectID); err == nil {
ownerID = o.String()
}
auth := registryAuthFor(db, ownerID, image) auth := registryAuthFor(db, ownerID, image)
deployReq.BuildConfig = &deployment.BuildConfig{ deployReq.BuildConfig = &deployment.BuildConfig{
BuildType: "prebuilt", BuildType: "prebuilt",
@@ -605,10 +642,11 @@ func runDeploymentAndSyncWithImage(
if msg := capacityCheck(parentCtx, db, engine.DockerClient(), service, replicas); msg != "" { if msg := capacityCheck(parentCtx, db, engine.DockerClient(), service, replicas); msg != "" {
failedAt := time.Now() failedAt := time.Now()
_, _ = db.Exec( _ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
`UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`, Error: sql.NullString{String: msg, Valid: true},
msg, failedAt, dbDeployment.ID, CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
) ID: dbDeployment.ID,
})
return return
} }
@@ -616,16 +654,16 @@ func runDeploymentAndSyncWithImage(
if err != nil { if err != nil {
failedAt := time.Now() failedAt := time.Now()
failure := "Failed to start deployment engine: " + err.Error() failure := "Failed to start deployment engine: " + err.Error()
_, _ = db.Exec( _ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
`UPDATE deployments Error: sql.NullString{String: failure, Valid: true},
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
WHERE id = $3`, ID: dbDeployment.ID,
failure, failedAt, dbDeployment.ID, })
) _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
_, _ = db.Exec( Status: sql.NullString{String: "failed", Valid: true},
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`, UpdatedAt: sql.NullTime{Time: failedAt, Valid: true},
failedAt, service.ID, ID: service.ID,
) })
return return
} }
@@ -640,16 +678,17 @@ func runDeploymentAndSyncWithImage(
if errors.Is(ctx.Err(), context.Canceled) { if errors.Is(ctx.Err(), context.Canceled) {
finalStatus, finalErr = "cancelled", "Deployment cancelled" finalStatus, finalErr = "cancelled", "Deployment cancelled"
} }
_, _ = db.Exec( _ = q.CompleteDeployment(ctx, sqlcdb.CompleteDeploymentParams{
`UPDATE deployments Status: sql.NullString{String: finalStatus, Valid: true},
SET status = $1, error = $2, completed_at = $3, updated_at = $3 Error: sql.NullString{String: finalErr, Valid: true},
WHERE id = $4`, CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
finalStatus, finalErr, failedAt, dbDeployment.ID, ID: dbDeployment.ID,
) })
_, _ = db.Exec( _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, Status: sql.NullString{String: finalStatus, Valid: true},
finalStatus, failedAt, service.ID, UpdatedAt: sql.NullTime{Time: failedAt, Valid: true},
) ID: service.ID,
})
return return
case <-syncTicker.C: case <-syncTicker.C:
current, getErr := engine.GetDeployment(engineDeployment.ID) current, getErr := engine.GetDeployment(engineDeployment.ID)
@@ -660,49 +699,36 @@ func runDeploymentAndSyncWithImage(
dbStatus := mapEngineStatusToDBStatus(current.Status) dbStatus := mapEngineStatusToDBStatus(current.Status)
imageName, imageTag := splitImageReference(current.ImageName, dbDeployment.ImageTag) imageName, imageTag := splitImageReference(current.ImageName, dbDeployment.ImageTag)
var dbError interface{} now := time.Now()
if current.Error != "" { _ = q.SyncDeploymentProgress(ctx, sqlcdb.SyncDeploymentProgressParams{
dbError = current.Error Status: sql.NullString{String: dbStatus, Valid: true},
} ImageName: sql.NullString{String: imageName, Valid: true},
ImageTag: sql.NullString{String: imageTag, Valid: true},
_, _ = db.Exec( BuildLog: sql.NullString{String: current.BuildLog, Valid: true},
`UPDATE deployments RuntimeLog: sql.NullString{String: current.DeployLog, Valid: true},
SET status = $1, Error: sql.NullString{String: current.Error, Valid: current.Error != ""},
image_name = $2, StartedAt: ntPtr(current.StartedAt),
image_tag = $3, CompletedAt: ntPtr(current.CompletedAt),
build_log = $4, UpdatedAt: sql.NullTime{Time: now, Valid: true},
runtime_log = $5, ID: dbDeployment.ID,
error = $6, })
started_at = $7,
completed_at = $8,
updated_at = $9
WHERE id = $10`,
dbStatus,
imageName,
imageTag,
current.BuildLog,
current.DeployLog,
dbError,
current.StartedAt,
current.CompletedAt,
time.Now(),
dbDeployment.ID,
)
switch dbStatus { switch dbStatus {
case "deployed": case "deployed":
_, _ = db.Exec( _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`, Status: sql.NullString{String: "running", Valid: true},
time.Now(), service.ID, UpdatedAt: sql.NullTime{Time: now, Valid: true},
) ID: service.ID,
})
insertUserNotification(db, userID, "deployment", "Deployment succeeded", insertUserNotification(db, userID, "deployment", "Deployment succeeded",
fmt.Sprintf("Service %s is now running.", service.Name), "service", service.ID.String()) fmt.Sprintf("Service %s is now running.", service.Name), "service", service.ID.String())
return return
case "failed": case "failed":
_, _ = db.Exec( _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`, Status: sql.NullString{String: "failed", Valid: true},
time.Now(), service.ID, UpdatedAt: sql.NullTime{Time: now, Valid: true},
) ID: service.ID,
})
body := fmt.Sprintf("Service %s failed to deploy.", service.Name) body := fmt.Sprintf("Service %s failed to deploy.", service.Name)
if current.Error != "" { if current.Error != "" {
body = fmt.Sprintf("Service %s failed to deploy: %s", service.Name, current.Error) body = fmt.Sprintf("Service %s failed to deploy: %s", service.Name, current.Error)
@@ -754,21 +780,16 @@ func handleCancelDeployment(c *gin.Context) {
return return
} }
var status, owner string ctx := c.Request.Context()
var serviceID uuid.UUID q := sqlcdb.New(db.(*database.DB).DB)
err = db.(*database.DB).QueryRow( access, err := q.GetDeploymentAccess(ctx, deploymentID)
`SELECT d.status, d.service_id, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1`,
deploymentID,
).Scan(&status, &serviceID, &owner)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found", "code": "NOT_FOUND"}) c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found", "code": "NOT_FOUND"})
return return
} }
if owner != userID.(string) && !contextIsAdmin(c) { serviceID := access.ServiceID
status := access.Status.String
if access.OwnerID.String() != userID.(string) && !contextIsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied", "code": "FORBIDDEN"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied", "code": "FORBIDDEN"})
return return
} }
@@ -782,14 +803,17 @@ func handleCancelDeployment(c *gin.Context) {
switch getDeployQueue(c).Cancel(serviceID, deploymentID) { switch getDeployQueue(c).Cancel(serviceID, deploymentID) {
case deployqueue.WasQueued: case deployqueue.WasQueued:
now := time.Now() now := time.Now()
_, _ = db.(*database.DB).Exec( _ = q.CompleteDeployment(ctx, sqlcdb.CompleteDeploymentParams{
`UPDATE deployments SET status = 'cancelled', error = 'Deployment cancelled', completed_at = $1, updated_at = $1 WHERE id = $2`, Status: sql.NullString{String: "cancelled", Valid: true},
now, deploymentID, Error: sql.NullString{String: "Deployment cancelled", Valid: true},
) CompletedAt: sql.NullTime{Time: now, Valid: true},
_, _ = db.(*database.DB).Exec( ID: deploymentID,
`UPDATE services SET status = 'cancelled', updated_at = $1 WHERE id = $2`, })
now, serviceID, _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
) Status: sql.NullString{String: "cancelled", Valid: true},
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: serviceID,
})
c.JSON(http.StatusOK, gin.H{"status": "cancelled"}) c.JSON(http.StatusOK, gin.H{"status": "cancelled"})
case deployqueue.WasActive: case deployqueue.WasActive:
c.JSON(http.StatusAccepted, gin.H{"status": "cancelling"}) c.JSON(http.StatusAccepted, gin.H{"status": "cancelling"})
@@ -832,27 +856,18 @@ func handleGetDeployment(c *gin.Context) {
return return
} }
var d DeploymentModel r, err := sqlcdb.New(db.(*database.DB).DB).GetDeploymentWithProject(c.Request.Context(), deploymentID)
var projectID uuid.UUID
err = db.(*database.DB).QueryRow(
`SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1`,
deploymentID,
).Scan(
&d.ID, &d.ServiceID, &d.CommitHash, &d.Status, &d.ImageName, &d.ImageTag,
&d.BuildLog, &d.RuntimeLog, &d.Error, &d.StartedAt, &d.CompletedAt,
&d.CreatedAt, &d.UpdatedAt, &projectID,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
return return
} }
projectID := r.ProjectID
d := deploymentModelFrom(deploymentRowShape{
ID: r.ID, ServiceID: r.ServiceID, CommitHash: r.CommitHash, Status: r.Status,
ImageName: r.ImageName, ImageTag: r.ImageTag, BuildLog: r.BuildLog,
RuntimeLog: r.RuntimeLog, Error: r.Error, StartedAt: r.StartedAt,
CompletedAt: r.CompletedAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
})
if _, allowed := projectReadAccess(c, db.(*database.DB), projectID); !allowed { if _, allowed := projectReadAccess(c, db.(*database.DB), projectID); !allowed {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
@@ -882,33 +897,23 @@ func handleRollbackDeployment(c *gin.Context) {
return return
} }
var targetDeployment DeploymentModel ctx := c.Request.Context()
var serviceID uuid.UUID q := sqlcdb.New(db.(*database.DB).DB)
var ownerCheck string r, err := q.GetDeploymentForRollback(ctx, deploymentID)
err = db.(*database.DB).QueryRow(
`SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1`,
deploymentID,
).Scan(
&targetDeployment.ID, &serviceID, &targetDeployment.CommitHash, &targetDeployment.Status,
&targetDeployment.ImageName, &targetDeployment.ImageTag, &targetDeployment.BuildLog,
&targetDeployment.RuntimeLog, &targetDeployment.Error, &targetDeployment.StartedAt,
&targetDeployment.CompletedAt, &targetDeployment.CreatedAt, &targetDeployment.UpdatedAt,
&ownerCheck,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
return return
} }
serviceID := r.ServiceID
targetDeployment := deploymentModelFrom(deploymentRowShape{
ID: r.ID, ServiceID: r.ServiceID, CommitHash: r.CommitHash, Status: r.Status,
ImageName: r.ImageName, ImageTag: r.ImageTag, BuildLog: r.BuildLog,
RuntimeLog: r.RuntimeLog, Error: r.Error, StartedAt: r.StartedAt,
CompletedAt: r.CompletedAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
})
ownerCheck := r.OwnerID
if ownerCheck != userID.(string) && !contextIsAdmin(c) { if ownerCheck.String() != userID.(string) && !contextIsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -931,23 +936,28 @@ func handleRollbackDeployment(c *gin.Context) {
UpdatedAt: now, UpdatedAt: now,
} }
_, err = db.(*database.DB).Exec( err = q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{
`INSERT INTO deployments ID: rollback.ID,
(id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at) ServiceID: rollback.ServiceID,
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, Version: fmt.Sprintf("rollback-%d", now.Unix()),
rollback.ID, rollback.ServiceID, fmt.Sprintf("rollback-%d", now.Unix()), rollback.CommitHash, rollback.Status, CommitHash: sql.NullString{String: ptrStr(rollback.CommitHash), Valid: rollback.CommitHash != nil},
rollback.ImageName, rollback.ImageTag, rollback.CreatedAt, rollback.UpdatedAt, Status: sql.NullString{String: rollback.Status, Valid: true},
) ImageName: sql.NullString{String: rollback.ImageName, Valid: true},
ImageTag: sql.NullString{String: rollback.ImageTag, Valid: true},
CreatedAt: sql.NullTime{Time: rollback.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: rollback.UpdatedAt, Valid: true},
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create rollback deployment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create rollback deployment"})
return return
} }
_, err = db.(*database.DB).Exec( err = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
`UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`, Status: sql.NullString{String: "building", Valid: true},
time.Now(), serviceID, UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
) ID: serviceID,
})
// Real rollback: redeploy the target deployment's image. // Real rollback: redeploy the target deployment's image.
engineValue, _ := c.Get("deployment_engine") engineValue, _ := c.Get("deployment_engine")
@@ -963,36 +973,18 @@ func handleRollbackDeployment(c *gin.Context) {
if engine == nil { if engine == nil {
reason = "Deployment engine unavailable. Docker may not be configured on this server." reason = "Deployment engine unavailable. Docker may not be configured on this server."
} }
_, _ = db.(*database.DB).Exec( _ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
`UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`, Error: sql.NullString{String: reason, Valid: true},
reason, completedAt, rollbackID, CompletedAt: sql.NullTime{Time: completedAt, Valid: true},
) ID: rollbackID,
_, _ = db.(*database.DB).Exec( })
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`, _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
completedAt, serviceID, Status: sql.NullString{String: "failed", Valid: true},
) UpdatedAt: sql.NullTime{Time: completedAt, Valid: true},
ID: serviceID,
})
} else { } else {
var service Service service, _ := loadServiceForDeploy(db.(*database.DB), serviceID)
_ = db.(*database.DB).QueryRow(
`SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at
FROM services s WHERE s.id = $1`, serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt,
)
rollbackReq := CreateDeploymentRequest{ rollbackReq := CreateDeploymentRequest{
CommitHash: func() string { CommitHash: func() string {
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"net/http" "net/http"
"regexp" "regexp"
@@ -210,7 +211,7 @@ func basicAuthUsernames(encoded string) []string {
// serviceWriteAccess mirrors the service update handler's access check. // serviceWriteAccess mirrors the service update handler's access check.
func serviceWriteAccess(c *gin.Context, db *database.DB, serviceID uuid.UUID) bool { func serviceWriteAccess(c *gin.Context, db *database.DB, serviceID uuid.UUID) bool {
userID, _ := c.Get("user_id") userID, _ := c.Get("user_id")
uid, _ := userID.(uuid.UUID) uid, _ := uuid.Parse(fmt.Sprint(userID))
ok, err := sqlcdb.New(db.DB).ServiceWriteAccess(context.Background(), sqlcdb.ServiceWriteAccessParams{ ok, err := sqlcdb.New(db.DB).ServiceWriteAccess(context.Background(), sqlcdb.ServiceWriteAccessParams{
ID: serviceID, ID: serviceID,
OwnerID: uid, OwnerID: uid,
+301
View File
@@ -2,9 +2,11 @@ package api
import ( import (
"context" "context"
"database/sql"
"os" "os"
"strings" "strings"
"testing" "testing"
"time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb" "containr/internal/database/sqlcdb"
@@ -143,3 +145,302 @@ func TestServiceDomainsLiveInvariants(t *testing.T) {
t.Fatalf("legacy after delete = %q, want %q", legacy, d1.Domain) t.Fatalf("legacy after delete = %q, want %q", legacy, d1.Domain)
} }
} }
// Live clone test: cloneServiceRow must copy config, domains, and variables
// verbatim, start stopped, and land in the target project/environment.
func TestCloneServiceRowLive(t *testing.T) {
dsn := testDSNString(t)
db, err := database.NewConnection(dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer db.Close()
ctx := context.Background()
q := sqlcdb.New(db.DB)
userID := uuid.New()
projectID := uuid.New()
envID := uuid.New()
serviceID := uuid.New()
suffix := strings.ReplaceAll(serviceID.String()[:8], "-", "")
must := func(err error) {
t.Helper()
if err != nil {
t.Fatalf("setup: %v", err)
}
}
_, err = db.Exec(`INSERT INTO users (id, email, password_hash, name) VALUES ($1, $2, 'x', 'test')`,
userID, "clone-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "clone-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type,
image_name, port, volumes, traefik_labels, maintenance_mode)
VALUES ($1, $2, $3, $4, 'web', 'image', 'traefik/whoami:latest', 80,
'[{"mount":"/data"}]'::jsonb,
'{"middlewares.rl.ratelimit.average":"100"}'::jsonb, true)`,
serviceID, "clonesrc-"+suffix, projectID, envID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }()
if _, err := q.CreateServiceDomain(ctx, sqlcdb.CreateServiceDomainParams{
ServiceID: serviceID, Domain: "src-" + suffix + ".example.com", IsDefault: true}); err != nil {
t.Fatalf("seed domain: %v", err)
}
if _, err := db.Exec(`INSERT INTO environment_variables (id, service_id, key, value, is_secret)
VALUES ($1, $2, 'FOO', 'bar', false)`, uuid.New(), serviceID); err != nil {
t.Fatalf("seed var: %v", err)
}
cloneID, err := cloneServiceRow(db, serviceID, projectID, "clone-"+suffix, "")
if err != nil {
t.Fatalf("clone: %v", err)
}
var status, imageName, env string
var maintenance bool
var volumes, labels []byte
if err := db.QueryRow(`SELECT status, image_name, COALESCE(environment,''), maintenance_mode,
volumes, traefik_labels FROM services WHERE id = $1`, cloneID).
Scan(&status, &imageName, &env, &maintenance, &volumes, &labels); err != nil {
t.Fatalf("read clone: %v", err)
}
if status != "stopped" || imageName != "traefik/whoami:latest" || env != "production" {
t.Fatalf("clone mismatch: %v %v %v", status, imageName, env)
}
if !maintenance || string(volumes) == "[]" || string(labels) == "{}" {
t.Fatalf("clone dropped config: maint=%v vol=%s labels=%s", maintenance, volumes, labels)
}
doms := loadServiceDomains(db, cloneID)
if len(doms) != 1 || doms[0].Domain != "src-"+suffix+".example.com" || !doms[0].IsDefault {
t.Fatalf("domains not cloned: %+v", doms)
}
var varVal string
if err := db.QueryRow(`SELECT value FROM environment_variables WHERE service_id = $1 AND key = 'FOO'`,
cloneID).Scan(&varVal); err != nil || varVal != "bar" {
t.Fatalf("vars not cloned: %v %q", err, varVal)
}
}
// Live preview-environment test: generated queries must preserve the
// access-check join, the status-sync CASE, and the sweep listing.
func TestPreviewEnvironmentsLiveInvariants(t *testing.T) {
dsn := testDSNString(t)
db, err := database.NewConnection(dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer db.Close()
ctx := context.Background()
q := sqlcdb.New(db.DB)
userID := uuid.New()
projectID := uuid.New()
envID := uuid.New()
srcID := uuid.New()
cloneID := uuid.New()
previewID := uuid.New()
suffix := strings.ReplaceAll(previewID.String()[:8], "-", "")
must := func(err error) {
t.Helper()
if err != nil {
t.Fatalf("setup: %v", err)
}
}
_, err = db.Exec(`INSERT INTO users (id, email, password_hash, name) VALUES ($1, $2, 'x', 'test')`,
userID, "prev-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "prev-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
for _, p := range [][3]interface{}{{srcID, "src-" + suffix, "running"}, {cloneID, "clone-" + suffix, "running"}} {
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type, status)
VALUES ($1, $2, $3, $4, 'web', 'image', $5)`,
p[0], p[1], projectID, envID, p[2])
must(err)
}
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }()
must(q.CreatePreviewEnvironment(ctx, sqlcdb.CreatePreviewEnvironmentParams{
ID: previewID,
ProjectID: projectID,
ServiceID: srcID,
PreviewServiceID: uuid.NullUUID{UUID: cloneID, Valid: true},
BranchName: "feat-" + suffix,
Environment: "preview-feat-" + suffix,
Status: "building",
ExpiresAt: time.Now().Add(-time.Hour), // already past TTL → sweepable
}))
// List + service join.
rows, err := q.ListPreviewEnvironmentsForProject(ctx, projectID)
if err != nil || len(rows) != 1 {
t.Fatalf("list: %v %d", err, len(rows))
}
if rows[0].ID != previewID || !rows[0].SvcID.Valid || rows[0].ServiceName.String != "src-"+suffix {
t.Fatalf("list row: %+v", rows[0])
}
// Access-check join: owner reads; a stranger does not.
if _, err := q.GetPreviewEnvironment(ctx, sqlcdb.GetPreviewEnvironmentParams{
ID: previewID, OwnerID: userID}); err != nil {
t.Fatalf("owner read: %v", err)
}
if _, err := q.GetPreviewEnvironment(ctx, sqlcdb.GetPreviewEnvironmentParams{
ID: previewID, OwnerID: uuid.New()}); err != nil {
t.Fatalf("approved project should be public-read: %v", err)
}
// Status sync folds clone's 'running' into the preview row.
if err := q.SyncPreviewStatuses(ctx); err != nil {
t.Fatalf("sync: %v", err)
}
var status string
if err := db.QueryRow(`SELECT status FROM preview_environments WHERE id = $1`, previewID).Scan(&status); err != nil {
t.Fatalf("read status: %v", err)
}
if status != "running" {
t.Fatalf("status after sync = %q, want running", status)
}
// TTL sweep picks it up (expired at insert), marks expired.
sweep, err := q.ListSweepablePreviews(ctx)
if err != nil {
t.Fatalf("sweep list: %v", err)
}
found := false
for _, s := range sweep {
if s.ID == previewID && s.PreviewServiceID.UUID == cloneID {
found = true
}
}
if !found {
t.Fatalf("sweepable list missing preview: %+v", sweep)
}
must(q.MarkPreviewEnvironmentStatus(ctx, sqlcdb.MarkPreviewEnvironmentStatusParams{Status: "expired", ID: previewID}))
sweep, _ = q.ListSweepablePreviews(ctx)
for _, s := range sweep {
if s.ID == previewID {
t.Fatal("expired preview still sweepable")
}
}
// Owner lookup and delete.
owner, err := q.GetPreviewEnvironmentOwner(ctx, previewID)
if err != nil || owner != userID {
t.Fatalf("owner: %v %v", owner, err)
}
must(q.DeletePreviewEnvironment(ctx, previewID))
}
// Live deployment lifecycle: generated queries preserve list ordering,
// the access join, and the status-transition writes.
func TestDeploymentsLiveInvariants(t *testing.T) {
dsn := testDSNString(t)
db, err := database.NewConnection(dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer db.Close()
ctx := context.Background()
q := sqlcdb.New(db.DB)
userID := uuid.New()
projectID := uuid.New()
envID := uuid.New()
serviceID := uuid.New()
deployID := uuid.New()
suffix := strings.ReplaceAll(deployID.String()[:8], "-", "")
must := func(err error) {
t.Helper()
if err != nil {
t.Fatalf("setup: %v", err)
}
}
_, err = db.Exec(`INSERT INTO users (id, email, password_hash, name) VALUES ($1, $2, 'x', 'test')`,
userID, "dep-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "dep-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type, status)
VALUES ($1, $2, $3, $4, 'web', 'image', 'running')`,
serviceID, "dep-"+suffix, projectID, envID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }()
must(q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{
ID: deployID,
ServiceID: serviceID,
Version: "v1-" + suffix,
Status: sql.NullString{String: "pending", Valid: true},
}))
total, err := q.CountDeploymentsForService(ctx, serviceID)
if err != nil || total != 1 {
t.Fatalf("count: %v %d", err, total)
}
rows, err := q.ListDeploymentsForService(ctx, sqlcdb.ListDeploymentsForServiceParams{
ServiceID: serviceID, Limit: 10, Offset: 0})
if err != nil || len(rows) != 1 || rows[0].ID != deployID {
t.Fatalf("list: %v %+v", err, rows)
}
// Access join returns owner.
access, err := q.GetDeploymentAccess(ctx, deployID)
if err != nil || access.OwnerID != userID || access.ServiceID != serviceID {
t.Fatalf("access: %v %+v", err, access)
}
// Recent feed honors the access predicate.
feed, err := q.ListRecentAccessibleDeployments(ctx, sqlcdb.ListRecentAccessibleDeploymentsParams{
OwnerID: userID, Limit: 10, Offset: 0})
if err != nil {
t.Fatalf("feed: %v", err)
}
found := false
for _, d := range feed {
if d.ID == deployID && d.ServiceName == "dep-"+suffix {
found = true
}
}
if !found {
t.Fatalf("feed missing deployment: %+v", feed)
}
// Status transitions.
must(q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{
Status: sql.NullString{String: "queued", Valid: true}, UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, ID: deployID}))
must(q.SyncDeploymentProgress(ctx, sqlcdb.SyncDeploymentProgressParams{
Status: sql.NullString{String: "deployed", Valid: true},
ImageName: sql.NullString{String: "nginx", Valid: true},
ImageTag: sql.NullString{String: "alpine", Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: deployID,
}))
r, err := q.GetDeploymentWithProject(ctx, deployID)
if err != nil {
t.Fatalf("get: %v", err)
}
if r.Status.String != "deployed" || r.ImageName.String != "nginx" || r.ProjectID != projectID {
t.Fatalf("post-sync row: %+v", r)
}
must(q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
Error: sql.NullString{String: "boom", Valid: true},
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: deployID,
}))
r, _ = q.GetDeploymentWithProject(ctx, deployID)
if r.Status.String != "failed" || r.Error.String != "boom" {
t.Fatalf("post-fail row: %+v", r)
}
}
+235 -279
View File
@@ -40,6 +40,68 @@ type PreviewEnvironment struct {
DeploymentID *uuid.UUID `json:"deployment_id,omitempty"` DeploymentID *uuid.UUID `json:"deployment_id,omitempty"`
} }
// previewRowShape is the common column set the generated preview rows
// carry — ListPreviewEnvironmentsForProjectRow and GetPreviewEnvironmentRow
// share it.
type previewRowShape struct {
ID uuid.UUID
ProjectID uuid.UUID
ServiceID uuid.UUID
PreviewServiceID uuid.NullUUID
BranchName string
PrNumber sql.NullInt32
Environment string
Status string
Url sql.NullString
ExpiresAt time.Time
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
SvcID uuid.NullUUID
ServiceName sql.NullString
ServiceType sql.NullString
}
func previewFromRow(r previewRowShape) PreviewEnvironment {
env := PreviewEnvironment{
ID: r.ID,
ProjectID: r.ProjectID,
ServiceID: r.ServiceID,
BranchName: r.BranchName,
Environment: r.Environment,
Status: r.Status,
URL: r.Url.String,
CreatedAt: r.CreatedAt.Time,
UpdatedAt: r.UpdatedAt.Time,
}
if r.PreviewServiceID.Valid {
id := r.PreviewServiceID.UUID
env.PreviewServiceID = &id
}
if r.PrNumber.Valid {
n := int(r.PrNumber.Int32)
env.PRNumber = &n
}
if !r.ExpiresAt.IsZero() {
t := r.ExpiresAt
env.ExpiresAt = &t
}
if r.SvcID.Valid {
env.Service = &Service{
ID: r.SvcID.UUID,
Name: r.ServiceName.String,
Type: r.ServiceType.String,
}
}
return env
}
func prNumberSQL(n *int) sql.NullInt32 {
if n == nil {
return sql.NullInt32{}
}
return sql.NullInt32{Int32: int32(*n), Valid: true}
}
// CreatePreviewEnvironmentRequest represents a request to create a preview environment // CreatePreviewEnvironmentRequest represents a request to create a preview environment
type CreatePreviewEnvironmentRequest struct { type CreatePreviewEnvironmentRequest struct {
ProjectID uuid.UUID `json:"project_id"` ProjectID uuid.UUID `json:"project_id"`
@@ -86,51 +148,21 @@ func handleGetPreviewEnvironments(c *gin.Context) {
} }
// Get preview environments for the project with service info // Get preview environments for the project with service info
rows, err := db.(*database.DB).Query( rows, err := sqlcdb.New(db.(*database.DB).DB).ListPreviewEnvironmentsForProject(c.Request.Context(), projectID)
`SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve preview environments"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve preview environments"})
return return
} }
defer rows.Close()
var environments []PreviewEnvironment environments := make([]PreviewEnvironment, 0, len(rows))
for rows.Next() { for _, r := range rows {
var env PreviewEnvironment environments = append(environments, previewFromRow(previewRowShape{
var serviceID sql.NullString ID: r.ID, ProjectID: r.ProjectID, ServiceID: r.ServiceID,
var serviceName sql.NullString PreviewServiceID: r.PreviewServiceID, BranchName: r.BranchName,
var serviceType sql.NullString PrNumber: r.PrNumber, Environment: r.Environment, Status: r.Status,
Url: r.Url, ExpiresAt: r.ExpiresAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
err := rows.Scan( SvcID: r.SvcID, ServiceName: r.ServiceName, ServiceType: r.ServiceType,
&env.ID, &env.ProjectID, &env.ServiceID, &env.PreviewServiceID, &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}) c.JSON(http.StatusOK, gin.H{"preview_environments": environments})
@@ -171,48 +203,37 @@ func handleCreatePreviewEnvironment(c *gin.Context) {
return return
} }
// Check if project exists and user has access ctx := c.Request.Context()
var project Project q := sqlcdb.New(db.(*database.DB).DB)
err = db.(*database.DB).QueryRow(
"SELECT id, name, owner_id FROM projects WHERE id = $1",
req.ProjectID,
).Scan(&project.ID, &project.Name, &project.OwnerID)
// Check if project exists and user has access
project, err := q.GetProjectBrief(ctx, req.ProjectID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
return return
} }
// Check if user owns the project // Check if user owns the project
if project.OwnerID != userID.(string) && !contextIsAdmin(c) { if project.OwnerID.String() != userID.(string) && !contextIsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
// Check if service exists and belongs to the project // Check if service exists and belongs to the project
var service Service service, err := q.GetServiceTypeBrief(ctx, sqlcdb.GetServiceTypeBriefParams{
err = db.(*database.DB).QueryRow( ID: req.ServiceID, ProjectID: req.ProjectID})
"SELECT id, name, COALESCE(type, service_type, '') FROM services WHERE id = $1 AND project_id = $2",
req.ServiceID, req.ProjectID,
).Scan(&service.ID, &service.Name, &service.Type)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found or doesn't belong to this project"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found or doesn't belong to this project"})
return return
} }
// Check if preview environment already exists for this branch and service // Check if preview environment already exists for this branch and service
var count int count, err := q.CountActivePreviewsForBranch(ctx, sqlcdb.CountActivePreviewsForBranchParams{
err = db.(*database.DB).QueryRow( ServiceID: req.ServiceID, BranchName: req.BranchName})
"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 { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing preview environment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing preview environment"})
return return
} }
if count > 0 { if count > 0 {
c.JSON(http.StatusConflict, gin.H{"error": "Preview environment already exists for this branch and service"}) c.JSON(http.StatusConflict, gin.H{"error": "Preview environment already exists for this branch and service"})
return return
@@ -237,29 +258,23 @@ func handleCreatePreviewEnvironment(c *gin.Context) {
// The clone's branch override is what makes it a preview — everything // The clone's branch override is what makes it a preview — everything
// else (env vars, volumes, builder, resources) stays identical. // else (env vars, volumes, builder, resources) stays identical.
if _, err := db.(*database.DB).Exec( if err := q.UpdateServicePreviewBranch(ctx, sqlcdb.UpdateServicePreviewBranchParams{
`UPDATE services SET git_branch = $1, domain = '', updated_at = NOW() WHERE id = $2`, GitBranch: sql.NullString{String: req.BranchName, Valid: true}, ID: cloneID}); err != nil {
req.BranchName, cloneID, _ = q.DeleteServiceByID(ctx, cloneID)
); err != nil {
_, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, cloneID)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure preview service"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure preview service"})
return return
} }
// Cloned service_domains would collide with the parent's Traefik // Cloned service_domains would collide with the parent's Traefik
// router — previews get their own hostname or none. // router — previews get their own hostname or none.
if _, err := db.(*database.DB).Exec( if err := q.DeleteAllServiceDomains(ctx, cloneID); err != nil {
`DELETE FROM service_domains WHERE service_id = $1`, cloneID, _ = q.DeleteServiceByID(ctx, cloneID)
); err != nil {
_, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, cloneID)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure preview domains"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure preview domains"})
return return
} }
previewHost := resolvePreviewHost(db.(*database.DB), cloneID, envName) previewHost := resolvePreviewHost(db.(*database.DB), cloneID, envName)
if previewHost != "" { if previewHost != "" {
_, _ = db.(*database.DB).Exec( _ = q.CreatePreviewDomain(ctx, sqlcdb.CreatePreviewDomainParams{
`INSERT INTO service_domains (service_id, domain, is_default, cert_type) VALUES ($1, $2, true, 'tls')`, ServiceID: cloneID, Domain: previewHost})
cloneID, previewHost,
)
} }
expiresAt := time.Now().Add(time.Duration(ttlHours) * time.Hour) expiresAt := time.Now().Add(time.Duration(ttlHours) * time.Hour)
@@ -279,30 +294,36 @@ func handleCreatePreviewEnvironment(c *gin.Context) {
env.URL = "https://" + previewHost env.URL = "https://" + previewHost
} }
_, err = db.(*database.DB).Exec( err = q.CreatePreviewEnvironment(ctx, sqlcdb.CreatePreviewEnvironmentParams{
`INSERT INTO preview_environments ID: env.ID,
(id, project_id, service_id, preview_service_id, branch_name, pr_number, environment, ProjectID: env.ProjectID,
status, url, expires_at, created_at, updated_at) ServiceID: env.ServiceID,
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, PreviewServiceID: uuid.NullUUID{UUID: cloneID, Valid: true},
env.ID, env.ProjectID, env.ServiceID, cloneID, env.BranchName, env.PRNumber, BranchName: env.BranchName,
env.Environment, env.Status, env.URL, env.ExpiresAt, env.CreatedAt, env.UpdatedAt, PrNumber: prNumberSQL(env.PRNumber),
) Environment: env.Environment,
Status: env.Status,
Url: sql.NullString{String: env.URL, Valid: env.URL != ""},
ExpiresAt: *env.ExpiresAt,
CreatedAt: sql.NullTime{Time: env.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: env.UpdatedAt, Valid: true},
})
if err != nil { if err != nil {
_, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, cloneID) _ = q.DeleteServiceByID(ctx, cloneID)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create preview environment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create preview environment"})
return return
} }
deploymentID := uuid.New() deploymentID := uuid.New()
_, err = db.(*database.DB).Exec( err = q.InsertPendingDeployment(ctx, sqlcdb.InsertPendingDeploymentParams{
`INSERT INTO deployments ID: deploymentID,
(id, service_id, version, commit_hash, status, created_at, updated_at) ServiceID: cloneID,
VALUES ($1, $2, $3, $4, 'pending', NOW(), NOW())`, Version: "preview-" + env.Environment,
deploymentID, cloneID, "preview-"+env.Environment, req.BranchName, CommitHash: sql.NullString{String: req.BranchName, Valid: true},
) })
if err != nil { if err != nil {
_, _ = db.(*database.DB).Exec(`DELETE FROM preview_environments WHERE id = $1`, env.ID) _ = q.DeletePreviewEnvironment(ctx, env.ID)
_, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, cloneID) _ = q.DeleteServiceByID(ctx, cloneID)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to provision preview deployment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to provision preview deployment"})
return return
} }
@@ -349,43 +370,20 @@ func handleGetPreviewEnvironment(c *gin.Context) {
// Public when the parent project is approved; otherwise owner/member/admin. // Public when the parent project is approved; otherwise owner/member/admin.
userID := optionalUserUUID(c) userID := optionalUserUUID(c)
var env PreviewEnvironment r, err := sqlcdb.New(db.(*database.DB).DB).GetPreviewEnvironment(
var serviceID sql.NullString c.Request.Context(), sqlcdb.GetPreviewEnvironmentParams{
var serviceName sql.NullString ID: envID, OwnerID: userID, Column3: contextIsAdmin(c)})
var serviceType sql.NullString
err = db.(*database.DB).QueryRow(
`SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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.is_approved OR p.owner_id = $2 OR $3::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $2))`,
envID, userID, contextIsAdmin(c),
).Scan(
&env.ID, &env.ProjectID, &env.ServiceID, &env.PreviewServiceID, &env.BranchName, &env.PRNumber,
&env.Environment, &env.Status, &env.URL, &env.ExpiresAt, &env.CreatedAt, &env.UpdatedAt,
&serviceID, &serviceName, &serviceType,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
return return
} }
env := previewFromRow(previewRowShape{
// Populate service info if available ID: r.ID, ProjectID: r.ProjectID, ServiceID: r.ServiceID,
if serviceID.Valid { PreviewServiceID: r.PreviewServiceID, BranchName: r.BranchName,
parsedServiceID, parseErr := uuid.Parse(serviceID.String) PrNumber: r.PrNumber, Environment: r.Environment, Status: r.Status,
if parseErr == nil { Url: r.Url, ExpiresAt: r.ExpiresAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
env.Service = &Service{ SvcID: r.SvcID, ServiceName: r.ServiceName, ServiceType: r.ServiceType,
ID: parsedServiceID, })
Name: serviceName.String,
Type: serviceType.String,
}
}
}
c.JSON(http.StatusOK, gin.H{"preview_environment": env}) c.JSON(http.StatusOK, gin.H{"preview_environment": env})
} }
@@ -418,24 +416,21 @@ func handleUpdatePreviewEnvironment(c *gin.Context) {
} }
// Check if preview environment exists and user has access // Check if preview environment exists and user has access
var existingEnv PreviewEnvironment uid, _ := uuid.Parse(fmt.Sprint(userID))
err = db.(*database.DB).QueryRow( q := sqlcdb.New(db.(*database.DB).DB)
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.pr_number, r, err := q.GetPreviewEnvironmentForWrite(
pe.environment, pe.status, pe.url, pe.expires_at, pe.created_at, pe.updated_at c.Request.Context(), sqlcdb.GetPreviewEnvironmentForWriteParams{
FROM preview_environments pe ID: envID, OwnerID: uid, Column3: contextIsAdmin(c)})
JOIN projects p ON pe.project_id = p.id
WHERE pe.id = $1 AND (p.owner_id = $2 OR $3::bool)`,
envID, userID, contextIsAdmin(c),
).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 { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
return return
} }
existingEnv := previewFromRow(previewRowShape{
ID: r.ID, ProjectID: r.ProjectID, ServiceID: r.ServiceID,
BranchName: r.BranchName, PrNumber: r.PrNumber, Environment: r.Environment,
Status: r.Status, Url: r.Url, ExpiresAt: r.ExpiresAt,
CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
})
// Update fields if provided // Update fields if provided
if req.Status != "" { if req.Status != "" {
@@ -455,12 +450,17 @@ func handleUpdatePreviewEnvironment(c *gin.Context) {
existingEnv.UpdatedAt = time.Now() existingEnv.UpdatedAt = time.Now()
// Update preview environment in database // Update preview environment in database
_, err = db.(*database.DB).Exec( expiresAt := time.Time{}
`UPDATE preview_environments if existingEnv.ExpiresAt != nil {
SET status = $1, url = $2, expires_at = $3, updated_at = $4 expiresAt = *existingEnv.ExpiresAt
WHERE id = $5`, }
existingEnv.Status, existingEnv.URL, existingEnv.ExpiresAt, existingEnv.UpdatedAt, existingEnv.ID, err = q.UpdatePreviewEnvironment(c.Request.Context(), sqlcdb.UpdatePreviewEnvironmentParams{
) Status: existingEnv.Status,
Url: sql.NullString{String: existingEnv.URL, Valid: existingEnv.URL != ""},
ExpiresAt: expiresAt,
UpdatedAt: sql.NullTime{Time: existingEnv.UpdatedAt, Valid: true},
ID: existingEnv.ID,
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preview environment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preview environment"})
@@ -492,46 +492,34 @@ func handleDeletePreviewEnvironment(c *gin.Context) {
return return
} }
// Check if preview environment exists and user has access ctx := c.Request.Context()
var projectOwnerID string q := sqlcdb.New(db.(*database.DB).DB)
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)
// Check if preview environment exists and user has access
projectOwnerID, err := q.GetPreviewEnvironmentOwner(ctx, envID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
return return
} }
// Check if user owns the project // Check if user owns the project
if projectOwnerID != userID.(string) && !contextIsAdmin(c) { if projectOwnerID.String() != userID.(string) && !contextIsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
// Tear down the clone's runtime and row — the preview is the clone. // Tear down the clone's runtime and row — the preview is the clone.
var cloneID *uuid.UUID if cloneID, err := q.GetPreviewServiceID(ctx, envID); err == nil && cloneID.Valid {
_ = db.(*database.DB).QueryRow(
`SELECT preview_service_id FROM preview_environments WHERE id = $1`, envID,
).Scan(&cloneID)
if cloneID != nil {
if engineValue, exists := c.Get("deployment_engine"); exists && engineValue != nil { if engineValue, exists := c.Get("deployment_engine"); exists && engineValue != nil {
if engine, ok := engineValue.(*deployment.DeploymentEngine); ok { if engine, ok := engineValue.(*deployment.DeploymentEngine); ok {
removeServiceRuntime(c.Request.Context(), db.(*database.DB), engine, *cloneID) removeServiceRuntime(ctx, db.(*database.DB), engine, cloneID.UUID)
} }
} }
_, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, *cloneID) _ = q.DeleteServiceByID(ctx, cloneID.UUID)
} }
// Delete preview environment // Delete preview environment
_, err = db.(*database.DB).Exec( err = q.DeletePreviewEnvironment(ctx, envID)
"DELETE FROM preview_environments WHERE id = $1",
envID,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete preview environment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete preview environment"})
@@ -569,22 +557,30 @@ func handlePromotePreviewEnvironment(c *gin.Context) {
return return
} }
// Get preview environment details ctx := c.Request.Context()
var env PreviewEnvironment q := sqlcdb.New(db.(*database.DB).DB)
err = db.(*database.DB).QueryRow(
`SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 OR $3::bool)`,
envID, userID, contextIsAdmin(c),
).Scan(
&env.ID, &env.ProjectID, &env.ServiceID, &env.PreviewServiceID, &env.BranchName, &env.Environment, &env.Status,
)
// Get preview environment details
uid, _ := uuid.Parse(fmt.Sprint(userID))
r, err := q.GetPreviewEnvironmentForPromote(
ctx, sqlcdb.GetPreviewEnvironmentForPromoteParams{
ID: envID, OwnerID: uid, Column3: contextIsAdmin(c)})
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
return return
} }
env := PreviewEnvironment{
ID: r.ID,
ProjectID: r.ProjectID,
ServiceID: r.ServiceID,
BranchName: r.BranchName,
Environment: r.Environment,
Status: r.Status,
}
if r.PreviewServiceID.Valid {
id := r.PreviewServiceID.UUID
env.PreviewServiceID = &id
}
// Check if preview environment is in a state that can be promoted // Check if preview environment is in a state that can be promoted
if env.Status != "running" { if env.Status != "running" {
@@ -596,11 +592,12 @@ func handlePromotePreviewEnvironment(c *gin.Context) {
// branch onto its own deployment track. // branch onto its own deployment track.
deploymentID := uuid.New() deploymentID := uuid.New()
promotionVersion := fmt.Sprintf("promote-%s-%d", strings.ReplaceAll(env.BranchName, "/", "-"), time.Now().Unix()) promotionVersion := fmt.Sprintf("promote-%s-%d", strings.ReplaceAll(env.BranchName, "/", "-"), time.Now().Unix())
_, err = db.(*database.DB).Exec( err = q.InsertPendingDeployment(ctx, sqlcdb.InsertPendingDeploymentParams{
`INSERT INTO deployments (id, service_id, version, commit_hash, status, created_at, updated_at) ID: deploymentID,
VALUES ($1, $2, $3, $4, 'pending', NOW(), NOW())`, ServiceID: env.ServiceID,
deploymentID, env.ServiceID, promotionVersion, env.BranchName, Version: promotionVersion,
) CommitHash: sql.NullString{String: env.BranchName, Valid: true},
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create promotion deployment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create promotion deployment"})
return return
@@ -628,18 +625,14 @@ func handlePromotePreviewEnvironment(c *gin.Context) {
if env.PreviewServiceID != nil { if env.PreviewServiceID != nil {
if engineValue, exists := c.Get("deployment_engine"); exists && engineValue != nil { if engineValue, exists := c.Get("deployment_engine"); exists && engineValue != nil {
if engine, ok := engineValue.(*deployment.DeploymentEngine); ok { if engine, ok := engineValue.(*deployment.DeploymentEngine); ok {
removeServiceRuntime(c.Request.Context(), db.(*database.DB), engine, *env.PreviewServiceID) removeServiceRuntime(ctx, db.(*database.DB), engine, *env.PreviewServiceID)
} }
} }
_, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, *env.PreviewServiceID) _ = q.DeleteServiceByID(ctx, *env.PreviewServiceID)
} }
if _, err := db.(*database.DB).Exec( if err := q.MarkPreviewEnvironmentStatus(ctx, sqlcdb.MarkPreviewEnvironmentStatusParams{
`UPDATE preview_environments Status: "stopped", ID: env.ID}); err != nil {
SET status = 'stopped', updated_at = NOW()
WHERE id = $1`,
env.ID,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preview environment status"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preview environment status"})
return return
} }
@@ -682,28 +675,27 @@ func handleCleanupExpiredPreviewEnvironments(c *gin.Context) {
return return
} }
ctx := c.Request.Context()
q := sqlcdb.New(db.(*database.DB).DB)
uid, _ := uuid.Parse(fmt.Sprint(userID))
// Find expired preview environments for user's projects // Find expired preview environments for user's projects
rows, err := db.(*database.DB).Query( rows, err := q.ListExpiredPreviewsForUser(ctx, sqlcdb.ListExpiredPreviewsForUserParams{
`SELECT pe.id, pe.project_id, pe.service_id, pe.preview_service_id, pe.branch_name, pe.environment OwnerID: uid, Column2: contextIsAdmin(c)})
FROM preview_environments pe
JOIN projects p ON pe.project_id = p.id
WHERE (p.owner_id = $1 OR $2::bool) AND pe.expires_at < NOW() AND pe.status != 'expired'`,
userID, contextIsAdmin(c),
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to find expired preview environments"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to find expired preview environments"})
return return
} }
defer rows.Close()
var expiredEnvs []PreviewEnvironment expiredEnvs := make([]PreviewEnvironment, 0, len(rows))
for rows.Next() { for _, r := range rows {
var env PreviewEnvironment env := PreviewEnvironment{
err := rows.Scan( ID: r.ID, ProjectID: r.ProjectID, ServiceID: r.ServiceID,
&env.ID, &env.ProjectID, &env.ServiceID, &env.PreviewServiceID, &env.BranchName, &env.Environment, BranchName: r.BranchName, Environment: r.Environment,
) }
if err != nil { if r.PreviewServiceID.Valid {
continue id := r.PreviewServiceID.UUID
env.PreviewServiceID = &id
} }
expiredEnvs = append(expiredEnvs, env) expiredEnvs = append(expiredEnvs, env)
} }
@@ -711,20 +703,17 @@ func handleCleanupExpiredPreviewEnvironments(c *gin.Context) {
// Mark expired environments as expired and tear down their clones // Mark expired environments as expired and tear down their clones
cleanupCount := 0 cleanupCount := 0
for _, env := range expiredEnvs { for _, env := range expiredEnvs {
_, err := db.(*database.DB).Exec( if err := q.MarkPreviewEnvironmentStatus(ctx, sqlcdb.MarkPreviewEnvironmentStatusParams{
"UPDATE preview_environments SET status = 'expired', updated_at = NOW() WHERE id = $1", Status: "expired", ID: env.ID}); err != nil {
env.ID,
)
if err != nil {
continue continue
} }
if env.PreviewServiceID != nil { if env.PreviewServiceID != nil {
if engineValue, exists := c.Get("deployment_engine"); exists && engineValue != nil { if engineValue, exists := c.Get("deployment_engine"); exists && engineValue != nil {
if engine, ok := engineValue.(*deployment.DeploymentEngine); ok { if engine, ok := engineValue.(*deployment.DeploymentEngine); ok {
removeServiceRuntime(c.Request.Context(), db.(*database.DB), engine, *env.PreviewServiceID) removeServiceRuntime(ctx, db.(*database.DB), engine, *env.PreviewServiceID)
} }
} }
_, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, *env.PreviewServiceID) _ = q.DeleteServiceByID(ctx, *env.PreviewServiceID)
} }
cleanupCount++ cleanupCount++
} }
@@ -754,15 +743,13 @@ func resolvePreviewHost(db *database.DB, cloneID uuid.UUID, envName string) stri
if label == "" { if label == "" {
return "" return ""
} }
var nodeID sql.NullString
var spread bool
if err := db.QueryRow(
`SELECT node_id, COALESCE(spread, false) FROM services WHERE id = $1`, cloneID,
).Scan(&nodeID, &spread); err != nil {
return ""
}
q := sqlcdb.New(db.DB) q := sqlcdb.New(db.DB)
ctx := context.Background() ctx := context.Background()
brief, err := q.GetServicePlacementBrief(ctx, cloneID)
if err != nil {
return ""
}
nodeID, spread := brief.NodeID, brief.Spread
var base string var base string
switch { switch {
case nodeID.Valid && nodeID.String != "": case nodeID.Valid && nodeID.String != "":
@@ -788,31 +775,27 @@ func resolvePreviewHost(db *database.DB, cloneID uuid.UUID, envName string) stri
// loadServiceForDeploy hydrates the Service fields runDeploymentAndSync // loadServiceForDeploy hydrates the Service fields runDeploymentAndSync
// needs — mirrors the loader in handleDeployService. // needs — mirrors the loader in handleDeployService.
func serviceForDeployFromRow(r sqlcdb.GetServiceForDeployRow) Service {
return Service{
ID: r.ID, ProjectID: r.ProjectID, Name: r.Name, Type: r.Type.String,
Status: r.Status.String, Image: r.Image.String, Command: r.Command.String,
Environment: r.Environment.String, GitRepo: r.GitRepo.String,
GitBranch: r.GitBranch.String, BuildPath: r.BuildPath.String,
CPU: r.Cpu.String, Memory: r.Memory.String,
Replicas: int(r.Replicas), Port: int(r.Port), Domain: r.Domain,
HealthCheckPath: r.HealthcheckPath, RestartPolicy: r.RestartPolicy,
Builder: r.Builder, CPUReserve: r.CpuReserve, MemoryReserve: r.MemoryReserve,
StaticBuildCmd: r.StaticBuildCmd, StaticDir: r.StaticDir,
CreatedAt: r.CreatedAt.Time, UpdatedAt: r.UpdatedAt.Time,
}
}
func loadServiceForDeploy(db *database.DB, serviceID uuid.UUID) (Service, error) { func loadServiceForDeploy(db *database.DB, serviceID uuid.UUID) (Service, error) {
var service Service r, err := sqlcdb.New(db.DB).GetServiceForDeploy(context.Background(), serviceID)
err := db.QueryRow( if err != nil {
`SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command, return Service{}, err
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory, }
COALESCE(s.replicas, 1), COALESCE(s.port, 0), return serviceForDeployFromRow(r), nil
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at
FROM services s
WHERE s.id = $1`,
serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt,
)
return service, err
} }
// StartPreviewSweeper expires preview environments past their TTL — tears // StartPreviewSweeper expires preview environments past their TTL — tears
@@ -838,54 +821,27 @@ func StartPreviewSweeper(ctx context.Context, db *database.DB, engine *deploymen
// sweepExpiredPreviews retires previews whose TTL elapsed. // sweepExpiredPreviews retires previews whose TTL elapsed.
func sweepExpiredPreviews(ctx context.Context, db *database.DB, engine *deployment.DeploymentEngine) { func sweepExpiredPreviews(ctx context.Context, db *database.DB, engine *deployment.DeploymentEngine) {
rows, err := db.QueryContext(ctx, q := sqlcdb.New(db.DB)
`SELECT id, preview_service_id FROM preview_environments rows, err := q.ListSweepablePreviews(ctx)
WHERE expires_at < NOW() AND status NOT IN ('expired', 'stopped')`)
if err != nil { if err != nil {
return return
} }
var ids []uuid.UUID var ids []uuid.UUID
var clones []uuid.UUID for _, r := range rows {
for rows.Next() { ids = append(ids, r.ID)
var id uuid.UUID if r.PreviewServiceID.Valid {
var cloneID *uuid.UUID removeServiceRuntime(ctx, db, engine, r.PreviewServiceID.UUID)
if err := rows.Scan(&id, &cloneID); err == nil { _ = q.DeleteServiceByID(ctx, r.PreviewServiceID.UUID)
ids = append(ids, id)
if cloneID != nil {
clones = append(clones, *cloneID)
} }
} }
}
rows.Close()
for _, cloneID := range clones {
removeServiceRuntime(ctx, db, engine, cloneID)
_, _ = db.ExecContext(ctx, `DELETE FROM services WHERE id = $1`, cloneID)
}
for _, id := range ids { for _, id := range ids {
_, _ = db.ExecContext(ctx, _ = q.MarkPreviewEnvironmentStatus(ctx, sqlcdb.MarkPreviewEnvironmentStatusParams{
`UPDATE preview_environments SET status = 'expired', updated_at = NOW() WHERE id = $1`, id) Status: "expired", ID: id})
} }
} }
// syncPreviewStatuses folds the clone's live service status into the // syncPreviewStatuses folds the clone's live service status into the
// preview row so list/detail endpoints reflect the real deploy state. // preview row so list/detail endpoints reflect the real deploy state.
func syncPreviewStatuses(ctx context.Context, db *database.DB) { func syncPreviewStatuses(ctx context.Context, db *database.DB) {
_, _ = db.ExecContext(ctx, ` _ = sqlcdb.New(db.DB).SyncPreviewStatuses(ctx)
UPDATE preview_environments pe
SET status = derived.st, updated_at = NOW()
FROM (
SELECT pe2.id,
CASE
WHEN s.status IN ('running', 'deployed') THEN 'running'
WHEN s.status IN ('failed', 'error') THEN 'failed'
WHEN s.status IN ('building', 'deploying', 'queued', 'pending', 'cloning') THEN 'building'
ELSE pe2.status
END AS st
FROM preview_environments pe2
JOIN services s ON pe2.preview_service_id = s.id
) derived
WHERE pe.id = derived.id
AND pe.status IN ('building', 'running', 'failed')
AND pe.status <> derived.st
`)
} }
+42 -60
View File
@@ -1,11 +1,14 @@
package api package api
import ( import (
"context"
"database/sql"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
@@ -53,11 +56,8 @@ func handleCloneService(c *gin.Context) {
if name == "" { if name == "" {
name = source.Name + "-copy" name = source.Name + "-copy"
} }
var count int count, _ := sqlcdb.New(db.(*database.DB).DB).CountServicesByName(
_ = db.(*database.DB).QueryRow( c.Request.Context(), sqlcdb.CountServicesByNameParams{ProjectID: targetProjectID, Name: name})
`SELECT COUNT(*) FROM services WHERE project_id = $1 AND name = $2`,
targetProjectID, name,
).Scan(&count)
if count > 0 { if count > 0 {
c.JSON(http.StatusConflict, gin.H{"error": "Service name already exists in target project", "code": "CONFLICT"}) c.JSON(http.StatusConflict, gin.H{"error": "Service name already exists in target project", "code": "CONFLICT"})
return return
@@ -84,8 +84,11 @@ func handleCloneService(c *gin.Context) {
// inherits the source's. Used by the clone endpoint and preview // inherits the source's. Used by the clone endpoint and preview
// environments, which additionally override git_branch and domains. // environments, which additionally override git_branch and domains.
func cloneServiceRow(db *database.DB, sourceID uuid.UUID, targetProjectID uuid.UUID, name, environment string) (uuid.UUID, error) { func cloneServiceRow(db *database.DB, sourceID uuid.UUID, targetProjectID uuid.UUID, name, environment string) (uuid.UUID, error) {
var sourceEnv string ctx := context.Background()
if err := db.QueryRow(`SELECT COALESCE(environment, '') FROM services WHERE id = $1`, sourceID).Scan(&sourceEnv); err != nil { q := sqlcdb.New(db.DB)
sourceEnv, err := q.GetServiceEnvironment(ctx, sourceID)
if err != nil {
return uuid.Nil, err return uuid.Nil, err
} }
if environment == "" { if environment == "" {
@@ -102,46 +105,31 @@ func cloneServiceRow(db *database.DB, sourceID uuid.UUID, targetProjectID uuid.U
maintenance, basicAuth := serviceAccess(db, sourceID) maintenance, basicAuth := serviceAccess(db, sourceID)
newID := uuid.New() newID := uuid.New()
now := time.Now() now := time.Now()
// string() so pq sends text — []byte would go as bytea and fail ::jsonb.
volumesJSON := string(loadServiceVolumesJSON(db, sourceID))
_, err = db.Exec( err = q.CloneServiceRow(ctx, sqlcdb.CloneServiceRowParams{
`INSERT INTO services ID: newID,
(id, project_id, name, environment_id, service_type, source_type, source_url, image_name, ProjectID: targetProjectID,
build_command, start_command, type, status, image, command, environment, Name: name,
git_repo, git_branch, build_path, cpu, memory, replicas, port, domain, EnvironmentID: environmentID,
healthcheck_path, restart_policy, volumes, maintenance_mode, basic_auth_users, Environment: sql.NullString{String: environment, Valid: true},
builder, cpu_reserve, memory_reserve, static_build_cmd, static_dir, MaintenanceMode: maintenance,
created_at, updated_at) BasicAuthUsers: basicAuth,
SELECT $1, $2, $3, $4, service_type, source_type, source_url, image_name, CreatedAt: sql.NullTime{Time: now, Valid: true},
build_command, start_command, type, 'stopped', image, command, $5, ID_2: sourceID,
git_repo, git_branch, build_path, cpu, memory, replicas, port, domain, })
healthcheck_path, restart_policy, $6::jsonb, $7, $8,
builder, cpu_reserve, memory_reserve, static_build_cmd, static_dir,
$9, $9
FROM services WHERE id = $10`,
newID, targetProjectID, name, environmentID, environment, volumesJSON,
maintenance, basicAuth, now, sourceID,
)
if err != nil { if err != nil {
return uuid.Nil, err return uuid.Nil, err
} }
if _, err := db.Exec( if err := q.CloneServiceDomains(ctx, sqlcdb.CloneServiceDomainsParams{
`INSERT INTO service_domains (service_id, domain, is_default, cert_type) ServiceID: newID, ServiceID_2: sourceID}); err != nil {
SELECT $1, domain, is_default, cert_type FROM service_domains WHERE service_id = $2`, _ = q.DeleteServiceByID(ctx, newID)
newID, sourceID,
); err != nil {
_, _ = db.Exec(`DELETE FROM services WHERE id = $1`, newID)
return uuid.Nil, err return uuid.Nil, err
} }
// Variable values are copied as stored — ciphertext stays ciphertext. // Variable values are copied as stored — ciphertext stays ciphertext.
if _, err := db.Exec( if err := q.CloneServiceVariables(ctx, sqlcdb.CloneServiceVariablesParams{
`INSERT INTO environment_variables (id, service_id, key, value, is_secret) ServiceID: newID, ServiceID_2: sourceID}); err != nil {
SELECT gen_random_uuid(), $1, key, value, is_secret FROM environment_variables WHERE service_id = $2`, _ = q.DeleteServiceByID(ctx, newID)
newID, sourceID,
); err != nil {
_, _ = db.Exec(`DELETE FROM services WHERE id = $1`, newID)
return uuid.Nil, err return uuid.Nil, err
} }
return newID, nil return newID, nil
@@ -187,11 +175,8 @@ func handleMoveService(c *gin.Context) {
return return
} }
var count int count, _ := sqlcdb.New(db.(*database.DB).DB).CountServicesByName(
_ = db.(*database.DB).QueryRow( c.Request.Context(), sqlcdb.CountServicesByNameParams{ProjectID: req.ProjectID, Name: service.Name})
`SELECT COUNT(*) FROM services WHERE project_id = $1 AND name = $2`,
req.ProjectID, service.Name,
).Scan(&count)
if count > 0 { if count > 0 {
c.JSON(http.StatusConflict, gin.H{"error": "A service with this name already exists in the target project", "code": "CONFLICT"}) c.JSON(http.StatusConflict, gin.H{"error": "A service with this name already exists in the target project", "code": "CONFLICT"})
return return
@@ -207,15 +192,18 @@ func handleMoveService(c *gin.Context) {
return return
} }
res, err := db.(*database.DB).Exec( n, err := sqlcdb.New(db.(*database.DB).DB).MoveServiceProject(
`UPDATE services SET project_id = $1, environment_id = $2, updated_at = $3 WHERE id = $4`, c.Request.Context(), sqlcdb.MoveServiceProjectParams{
req.ProjectID, environmentID, time.Now(), serviceID, ProjectID: req.ProjectID,
) EnvironmentID: environmentID,
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: serviceID,
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to move service"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to move service"})
return return
} }
if n, _ := res.RowsAffected(); n == 0 { if n == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found", "code": "NOT_FOUND"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found", "code": "NOT_FOUND"})
return return
} }
@@ -229,16 +217,10 @@ func ownsProject(db *database.DB, projectID uuid.UUID, userID string, isAdmin bo
if isAdmin { if isAdmin {
return true return true
} }
var owner string uid, err := uuid.Parse(userID)
err := db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, projectID).Scan(&owner) if err != nil {
return err == nil && owner == userID return false
} }
owner, err := sqlcdb.New(db.DB).GetProjectOwner(context.Background(), projectID)
// loadServiceVolumesJSON returns the raw stored mounts for a verbatim copy. return err == nil && owner == uid
func loadServiceVolumesJSON(db *database.DB, serviceID uuid.UUID) []byte {
var raw []byte
if err := db.QueryRow(`SELECT volumes FROM services WHERE id = $1`, serviceID).Scan(&raw); err != nil || len(raw) == 0 {
return []byte("[]")
}
return raw
} }
@@ -0,0 +1,536 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: deployments.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const completeDeployment = `-- name: CompleteDeployment :exec
UPDATE deployments
SET status = $1, error = $2, completed_at = $3, updated_at = $3
WHERE id = $4
`
type CompleteDeploymentParams struct {
Status sql.NullString `json:"status"`
Error sql.NullString `json:"error"`
CompletedAt sql.NullTime `json:"completed_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error {
_, err := q.db.ExecContext(ctx, completeDeployment,
arg.Status,
arg.Error,
arg.CompletedAt,
arg.ID,
)
return err
}
const countAccessibleDeployments = `-- name: CountAccessibleDeployments :one
SELECT COUNT(*)
FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)
`
type CountAccessibleDeploymentsParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Column2 bool `json:"column_2"`
}
func (q *Queries) CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countAccessibleDeployments, arg.OwnerID, arg.Column2)
var count int64
err := row.Scan(&count)
return count, err
}
const countDeploymentsForService = `-- name: CountDeploymentsForService :one
SELECT COUNT(*) FROM deployments WHERE service_id = $1
`
func (q *Queries) CountDeploymentsForService(ctx context.Context, serviceID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, countDeploymentsForService, serviceID)
var count int64
err := row.Scan(&count)
return count, err
}
const failDeployment = `-- name: FailDeployment :exec
UPDATE deployments
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
WHERE id = $3
`
type FailDeploymentParams struct {
Error sql.NullString `json:"error"`
CompletedAt sql.NullTime `json:"completed_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) FailDeployment(ctx context.Context, arg FailDeploymentParams) error {
_, err := q.db.ExecContext(ctx, failDeployment, arg.Error, arg.CompletedAt, arg.ID)
return err
}
const getDeploymentAccess = `-- name: GetDeploymentAccess :one
SELECT d.status, d.service_id, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1
`
type GetDeploymentAccessRow struct {
Status sql.NullString `json:"status"`
ServiceID uuid.UUID `json:"service_id"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetDeploymentAccess(ctx context.Context, id uuid.UUID) (GetDeploymentAccessRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentAccess, id)
var i GetDeploymentAccessRow
err := row.Scan(&i.Status, &i.ServiceID, &i.OwnerID)
return i, err
}
const getDeploymentForRollback = `-- name: GetDeploymentForRollback :one
SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1
`
type GetDeploymentForRollbackRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
CommitHash sql.NullString `json:"commit_hash"`
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
BuildLog sql.NullString `json:"build_log"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentForRollback, id)
var i GetDeploymentForRollbackRow
err := row.Scan(
&i.ID,
&i.ServiceID,
&i.CommitHash,
&i.Status,
&i.ImageName,
&i.ImageTag,
&i.BuildLog,
&i.RuntimeLog,
&i.Error,
&i.StartedAt,
&i.CompletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
const getDeploymentWithProject = `-- name: GetDeploymentWithProject :one
SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.id AS project_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1
`
type GetDeploymentWithProjectRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
CommitHash sql.NullString `json:"commit_hash"`
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
BuildLog sql.NullString `json:"build_log"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
ProjectID uuid.UUID `json:"project_id"`
}
func (q *Queries) GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentWithProject, id)
var i GetDeploymentWithProjectRow
err := row.Scan(
&i.ID,
&i.ServiceID,
&i.CommitHash,
&i.Status,
&i.ImageName,
&i.ImageTag,
&i.BuildLog,
&i.RuntimeLog,
&i.Error,
&i.StartedAt,
&i.CompletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ProjectID,
)
return i, err
}
const getServiceForDeployWithOwner = `-- name: GetServiceForDeployWithOwner :one
SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1) AS replicas, COALESCE(s.port, 0) AS port,
COALESCE(s.domain, '') AS domain, COALESCE(s.healthcheck_path, '') AS healthcheck_path,
COALESCE(s.restart_policy, 'unless-stopped') AS restart_policy,
COALESCE(s.builder, 'auto') AS builder, COALESCE(s.cpu_reserve, '') AS cpu_reserve,
COALESCE(s.memory_reserve, '') AS memory_reserve, COALESCE(s.static_build_cmd, '') AS static_build_cmd,
COALESCE(s.static_dir, '') AS static_dir,
s.created_at, s.updated_at, p.owner_id
FROM services s
JOIN projects p ON s.project_id = p.id
WHERE s.id = $1
`
type GetServiceForDeployWithOwnerRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Type sql.NullString `json:"type"`
Status sql.NullString `json:"status"`
Image sql.NullString `json:"image"`
Command sql.NullString `json:"command"`
Environment sql.NullString `json:"environment"`
GitRepo sql.NullString `json:"git_repo"`
GitBranch sql.NullString `json:"git_branch"`
BuildPath sql.NullString `json:"build_path"`
Cpu sql.NullString `json:"cpu"`
Memory sql.NullString `json:"memory"`
Replicas int32 `json:"replicas"`
Port int32 `json:"port"`
Domain string `json:"domain"`
HealthcheckPath string `json:"healthcheck_path"`
RestartPolicy string `json:"restart_policy"`
Builder string `json:"builder"`
CpuReserve string `json:"cpu_reserve"`
MemoryReserve string `json:"memory_reserve"`
StaticBuildCmd string `json:"static_build_cmd"`
StaticDir string `json:"static_dir"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetServiceForDeployWithOwner(ctx context.Context, id uuid.UUID) (GetServiceForDeployWithOwnerRow, error) {
row := q.db.QueryRowContext(ctx, getServiceForDeployWithOwner, id)
var i GetServiceForDeployWithOwnerRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Type,
&i.Status,
&i.Image,
&i.Command,
&i.Environment,
&i.GitRepo,
&i.GitBranch,
&i.BuildPath,
&i.Cpu,
&i.Memory,
&i.Replicas,
&i.Port,
&i.Domain,
&i.HealthcheckPath,
&i.RestartPolicy,
&i.Builder,
&i.CpuReserve,
&i.MemoryReserve,
&i.StaticBuildCmd,
&i.StaticDir,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
const getServicePublishedPort = `-- name: GetServicePublishedPort :one
SELECT published_port FROM services WHERE id = $1
`
func (q *Queries) GetServicePublishedPort(ctx context.Context, id uuid.UUID) (int32, error) {
row := q.db.QueryRowContext(ctx, getServicePublishedPort, id)
var published_port int32
err := row.Scan(&published_port)
return published_port, err
}
const insertDeployment = `-- name: InsertDeployment :exec
INSERT INTO deployments
(id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
type InsertDeploymentParams struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
Version string `json:"version"`
CommitHash sql.NullString `json:"commit_hash"`
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) InsertDeployment(ctx context.Context, arg InsertDeploymentParams) error {
_, err := q.db.ExecContext(ctx, insertDeployment,
arg.ID,
arg.ServiceID,
arg.Version,
arg.CommitHash,
arg.Status,
arg.ImageName,
arg.ImageTag,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const listDeploymentsForService = `-- name: ListDeploymentsForService :many
SELECT id, service_id, commit_hash, status, image_name, image_tag,
build_log, runtime_log, error, started_at, completed_at, created_at, updated_at
FROM deployments
WHERE service_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
type ListDeploymentsForServiceParams struct {
ServiceID uuid.UUID `json:"service_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListDeploymentsForServiceRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
CommitHash sql.NullString `json:"commit_hash"`
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
BuildLog sql.NullString `json:"build_log"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) ListDeploymentsForService(ctx context.Context, arg ListDeploymentsForServiceParams) ([]ListDeploymentsForServiceRow, error) {
rows, err := q.db.QueryContext(ctx, listDeploymentsForService, arg.ServiceID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListDeploymentsForServiceRow{}
for rows.Next() {
var i ListDeploymentsForServiceRow
if err := rows.Scan(
&i.ID,
&i.ServiceID,
&i.CommitHash,
&i.Status,
&i.ImageName,
&i.ImageTag,
&i.BuildLog,
&i.RuntimeLog,
&i.Error,
&i.StartedAt,
&i.CompletedAt,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRecentAccessibleDeployments = `-- name: ListRecentAccessibleDeployments :many
SELECT d.id, d.service_id, s.name AS service_name, p.name AS project_name, d.status,
COALESCE(d.image_name, '') AS image_name, d.started_at, d.completed_at, d.created_at
FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)
ORDER BY d.created_at DESC
LIMIT $3 OFFSET $4
`
type ListRecentAccessibleDeploymentsParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Column2 bool `json:"column_2"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListRecentAccessibleDeploymentsRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
ServiceName string `json:"service_name"`
ProjectName string `json:"project_name"`
Status sql.NullString `json:"status"`
ImageName string `json:"image_name"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) ListRecentAccessibleDeployments(ctx context.Context, arg ListRecentAccessibleDeploymentsParams) ([]ListRecentAccessibleDeploymentsRow, error) {
rows, err := q.db.QueryContext(ctx, listRecentAccessibleDeployments,
arg.OwnerID,
arg.Column2,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListRecentAccessibleDeploymentsRow{}
for rows.Next() {
var i ListRecentAccessibleDeploymentsRow
if err := rows.Scan(
&i.ID,
&i.ServiceID,
&i.ServiceName,
&i.ProjectName,
&i.Status,
&i.ImageName,
&i.StartedAt,
&i.CompletedAt,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setDeploymentStatus = `-- name: SetDeploymentStatus :exec
UPDATE deployments SET status = $1, updated_at = $2 WHERE id = $3
`
type SetDeploymentStatusParams struct {
Status sql.NullString `json:"status"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetDeploymentStatus(ctx context.Context, arg SetDeploymentStatusParams) error {
_, err := q.db.ExecContext(ctx, setDeploymentStatus, arg.Status, arg.UpdatedAt, arg.ID)
return err
}
const setServiceStatus = `-- name: SetServiceStatus :exec
UPDATE services SET status = $1, updated_at = $2 WHERE id = $3
`
type SetServiceStatusParams struct {
Status sql.NullString `json:"status"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetServiceStatus(ctx context.Context, arg SetServiceStatusParams) error {
_, err := q.db.ExecContext(ctx, setServiceStatus, arg.Status, arg.UpdatedAt, arg.ID)
return err
}
const syncDeploymentProgress = `-- name: SyncDeploymentProgress :exec
UPDATE deployments
SET status = $1,
image_name = $2,
image_tag = $3,
build_log = $4,
runtime_log = $5,
error = $6,
started_at = $7,
completed_at = $8,
updated_at = $9
WHERE id = $10
`
type SyncDeploymentProgressParams struct {
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
BuildLog sql.NullString `json:"build_log"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SyncDeploymentProgress(ctx context.Context, arg SyncDeploymentProgressParams) error {
_, err := q.db.ExecContext(ctx, syncDeploymentProgress,
arg.Status,
arg.ImageName,
arg.ImageTag,
arg.BuildLog,
arg.RuntimeLog,
arg.Error,
arg.StartedAt,
arg.CompletedAt,
arg.UpdatedAt,
arg.ID,
)
return err
}
@@ -138,9 +138,20 @@ type DatabaseService struct {
type Deployment struct { type Deployment struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"` ServiceID uuid.UUID `json:"service_id"`
Version string `json:"version"`
CommitHash sql.NullString `json:"commit_hash"`
ImageDigest sql.NullString `json:"image_digest"`
Status sql.NullString `json:"status"` Status sql.NullString `json:"status"`
BuildLog sql.NullString `json:"build_log"`
DeploymentLog sql.NullString `json:"deployment_log"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
} }
type Environment struct { type Environment struct {
@@ -274,6 +285,21 @@ type OutboundWebhook struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type PreviewEnvironment struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
BranchName string `json:"branch_name"`
PrNumber sql.NullInt32 `json:"pr_number"`
Environment string `json:"environment"`
Status string `json:"status"`
Url sql.NullString `json:"url"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
PreviewServiceID uuid.NullUUID `json:"preview_service_id"`
}
type Project struct { type Project struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -0,0 +1,635 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: preview_environments.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const countActivePreviewsForBranch = `-- name: CountActivePreviewsForBranch :one
SELECT COUNT(*) FROM preview_environments
WHERE service_id = $1 AND branch_name = $2 AND status NOT IN ('expired', 'stopped')
`
type CountActivePreviewsForBranchParams struct {
ServiceID uuid.UUID `json:"service_id"`
BranchName string `json:"branch_name"`
}
func (q *Queries) CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countActivePreviewsForBranch, arg.ServiceID, arg.BranchName)
var count int64
err := row.Scan(&count)
return count, err
}
const createPreviewDomain = `-- name: CreatePreviewDomain :exec
INSERT INTO service_domains (service_id, domain, is_default, cert_type) VALUES ($1, $2, true, 'tls')
`
type CreatePreviewDomainParams struct {
ServiceID uuid.UUID `json:"service_id"`
Domain string `json:"domain"`
}
func (q *Queries) CreatePreviewDomain(ctx context.Context, arg CreatePreviewDomainParams) error {
_, err := q.db.ExecContext(ctx, createPreviewDomain, arg.ServiceID, arg.Domain)
return err
}
const createPreviewEnvironment = `-- name: CreatePreviewEnvironment :exec
INSERT INTO preview_environments
(id, project_id, service_id, preview_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, $12)
`
type CreatePreviewEnvironmentParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
PreviewServiceID uuid.NullUUID `json:"preview_service_id"`
BranchName string `json:"branch_name"`
PrNumber sql.NullInt32 `json:"pr_number"`
Environment string `json:"environment"`
Status string `json:"status"`
Url sql.NullString `json:"url"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) CreatePreviewEnvironment(ctx context.Context, arg CreatePreviewEnvironmentParams) error {
_, err := q.db.ExecContext(ctx, createPreviewEnvironment,
arg.ID,
arg.ProjectID,
arg.ServiceID,
arg.PreviewServiceID,
arg.BranchName,
arg.PrNumber,
arg.Environment,
arg.Status,
arg.Url,
arg.ExpiresAt,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const deleteAllServiceDomains = `-- name: DeleteAllServiceDomains :exec
DELETE FROM service_domains WHERE service_id = $1
`
func (q *Queries) DeleteAllServiceDomains(ctx context.Context, serviceID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteAllServiceDomains, serviceID)
return err
}
const deletePreviewEnvironment = `-- name: DeletePreviewEnvironment :exec
DELETE FROM preview_environments WHERE id = $1
`
func (q *Queries) DeletePreviewEnvironment(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deletePreviewEnvironment, id)
return err
}
const getPreviewEnvironment = `-- name: GetPreviewEnvironment :one
SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 svc_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.is_approved OR p.owner_id = $2 OR $3::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $2))
`
type GetPreviewEnvironmentParams struct {
ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"`
Column3 bool `json:"column_3"`
}
type GetPreviewEnvironmentRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
PreviewServiceID uuid.NullUUID `json:"preview_service_id"`
BranchName string `json:"branch_name"`
PrNumber sql.NullInt32 `json:"pr_number"`
Environment string `json:"environment"`
Status string `json:"status"`
Url sql.NullString `json:"url"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
SvcID uuid.NullUUID `json:"svc_id"`
ServiceName sql.NullString `json:"service_name"`
ServiceType sql.NullString `json:"service_type"`
}
func (q *Queries) GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error) {
row := q.db.QueryRowContext(ctx, getPreviewEnvironment, arg.ID, arg.OwnerID, arg.Column3)
var i GetPreviewEnvironmentRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.PreviewServiceID,
&i.BranchName,
&i.PrNumber,
&i.Environment,
&i.Status,
&i.Url,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.SvcID,
&i.ServiceName,
&i.ServiceType,
)
return i, err
}
const getPreviewEnvironmentForPromote = `-- name: GetPreviewEnvironmentForPromote :one
SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 OR $3::bool)
`
type GetPreviewEnvironmentForPromoteParams struct {
ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"`
Column3 bool `json:"column_3"`
}
type GetPreviewEnvironmentForPromoteRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
PreviewServiceID uuid.NullUUID `json:"preview_service_id"`
BranchName string `json:"branch_name"`
Environment string `json:"environment"`
Status string `json:"status"`
}
func (q *Queries) GetPreviewEnvironmentForPromote(ctx context.Context, arg GetPreviewEnvironmentForPromoteParams) (GetPreviewEnvironmentForPromoteRow, error) {
row := q.db.QueryRowContext(ctx, getPreviewEnvironmentForPromote, arg.ID, arg.OwnerID, arg.Column3)
var i GetPreviewEnvironmentForPromoteRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.PreviewServiceID,
&i.BranchName,
&i.Environment,
&i.Status,
)
return i, err
}
const getPreviewEnvironmentForWrite = `-- name: GetPreviewEnvironmentForWrite :one
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 OR $3::bool)
`
type GetPreviewEnvironmentForWriteParams struct {
ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"`
Column3 bool `json:"column_3"`
}
type GetPreviewEnvironmentForWriteRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
BranchName string `json:"branch_name"`
PrNumber sql.NullInt32 `json:"pr_number"`
Environment string `json:"environment"`
Status string `json:"status"`
Url sql.NullString `json:"url"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) GetPreviewEnvironmentForWrite(ctx context.Context, arg GetPreviewEnvironmentForWriteParams) (GetPreviewEnvironmentForWriteRow, error) {
row := q.db.QueryRowContext(ctx, getPreviewEnvironmentForWrite, arg.ID, arg.OwnerID, arg.Column3)
var i GetPreviewEnvironmentForWriteRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.BranchName,
&i.PrNumber,
&i.Environment,
&i.Status,
&i.Url,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getPreviewEnvironmentOwner = `-- name: GetPreviewEnvironmentOwner :one
SELECT p.owner_id
FROM preview_environments pe
JOIN projects p ON pe.project_id = p.id
WHERE pe.id = $1
`
func (q *Queries) GetPreviewEnvironmentOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getPreviewEnvironmentOwner, id)
var owner_id uuid.UUID
err := row.Scan(&owner_id)
return owner_id, err
}
const getPreviewServiceID = `-- name: GetPreviewServiceID :one
SELECT preview_service_id FROM preview_environments WHERE id = $1
`
func (q *Queries) GetPreviewServiceID(ctx context.Context, id uuid.UUID) (uuid.NullUUID, error) {
row := q.db.QueryRowContext(ctx, getPreviewServiceID, id)
var preview_service_id uuid.NullUUID
err := row.Scan(&preview_service_id)
return preview_service_id, err
}
const getProjectBrief = `-- name: GetProjectBrief :one
SELECT id, name, owner_id FROM projects WHERE id = $1
`
type GetProjectBriefRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetProjectBrief(ctx context.Context, id uuid.UUID) (GetProjectBriefRow, error) {
row := q.db.QueryRowContext(ctx, getProjectBrief, id)
var i GetProjectBriefRow
err := row.Scan(&i.ID, &i.Name, &i.OwnerID)
return i, err
}
const getServiceForDeploy = `-- name: GetServiceForDeploy :one
SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1) AS replicas, COALESCE(s.port, 0) AS port,
COALESCE(s.domain, '') AS domain, COALESCE(s.healthcheck_path, '') AS healthcheck_path,
COALESCE(s.restart_policy, 'unless-stopped') AS restart_policy,
COALESCE(s.builder, 'auto') AS builder, COALESCE(s.cpu_reserve, '') AS cpu_reserve,
COALESCE(s.memory_reserve, '') AS memory_reserve, COALESCE(s.static_build_cmd, '') AS static_build_cmd,
COALESCE(s.static_dir, '') AS static_dir,
s.created_at, s.updated_at
FROM services s
WHERE s.id = $1
`
type GetServiceForDeployRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Type sql.NullString `json:"type"`
Status sql.NullString `json:"status"`
Image sql.NullString `json:"image"`
Command sql.NullString `json:"command"`
Environment sql.NullString `json:"environment"`
GitRepo sql.NullString `json:"git_repo"`
GitBranch sql.NullString `json:"git_branch"`
BuildPath sql.NullString `json:"build_path"`
Cpu sql.NullString `json:"cpu"`
Memory sql.NullString `json:"memory"`
Replicas int32 `json:"replicas"`
Port int32 `json:"port"`
Domain string `json:"domain"`
HealthcheckPath string `json:"healthcheck_path"`
RestartPolicy string `json:"restart_policy"`
Builder string `json:"builder"`
CpuReserve string `json:"cpu_reserve"`
MemoryReserve string `json:"memory_reserve"`
StaticBuildCmd string `json:"static_build_cmd"`
StaticDir string `json:"static_dir"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) GetServiceForDeploy(ctx context.Context, id uuid.UUID) (GetServiceForDeployRow, error) {
row := q.db.QueryRowContext(ctx, getServiceForDeploy, id)
var i GetServiceForDeployRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Type,
&i.Status,
&i.Image,
&i.Command,
&i.Environment,
&i.GitRepo,
&i.GitBranch,
&i.BuildPath,
&i.Cpu,
&i.Memory,
&i.Replicas,
&i.Port,
&i.Domain,
&i.HealthcheckPath,
&i.RestartPolicy,
&i.Builder,
&i.CpuReserve,
&i.MemoryReserve,
&i.StaticBuildCmd,
&i.StaticDir,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getServiceTypeBrief = `-- name: GetServiceTypeBrief :one
SELECT id, name, COALESCE(type, service_type, '') AS service_type FROM services WHERE id = $1 AND project_id = $2
`
type GetServiceTypeBriefParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
}
type GetServiceTypeBriefRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
ServiceType string `json:"service_type"`
}
func (q *Queries) GetServiceTypeBrief(ctx context.Context, arg GetServiceTypeBriefParams) (GetServiceTypeBriefRow, error) {
row := q.db.QueryRowContext(ctx, getServiceTypeBrief, arg.ID, arg.ProjectID)
var i GetServiceTypeBriefRow
err := row.Scan(&i.ID, &i.Name, &i.ServiceType)
return i, err
}
const insertPendingDeployment = `-- name: InsertPendingDeployment :exec
INSERT INTO deployments (id, service_id, version, commit_hash, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'pending', NOW(), NOW())
`
type InsertPendingDeploymentParams struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
Version string `json:"version"`
CommitHash sql.NullString `json:"commit_hash"`
}
func (q *Queries) InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error {
_, err := q.db.ExecContext(ctx, insertPendingDeployment,
arg.ID,
arg.ServiceID,
arg.Version,
arg.CommitHash,
)
return err
}
const listExpiredPreviewsForUser = `-- name: ListExpiredPreviewsForUser :many
SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 OR $2::bool) AND pe.expires_at < NOW() AND pe.status != 'expired'
`
type ListExpiredPreviewsForUserParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Column2 bool `json:"column_2"`
}
type ListExpiredPreviewsForUserRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
PreviewServiceID uuid.NullUUID `json:"preview_service_id"`
BranchName string `json:"branch_name"`
Environment string `json:"environment"`
}
func (q *Queries) ListExpiredPreviewsForUser(ctx context.Context, arg ListExpiredPreviewsForUserParams) ([]ListExpiredPreviewsForUserRow, error) {
rows, err := q.db.QueryContext(ctx, listExpiredPreviewsForUser, arg.OwnerID, arg.Column2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListExpiredPreviewsForUserRow{}
for rows.Next() {
var i ListExpiredPreviewsForUserRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.PreviewServiceID,
&i.BranchName,
&i.Environment,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPreviewEnvironmentsForProject = `-- name: ListPreviewEnvironmentsForProject :many
SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 svc_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
`
type ListPreviewEnvironmentsForProjectRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
PreviewServiceID uuid.NullUUID `json:"preview_service_id"`
BranchName string `json:"branch_name"`
PrNumber sql.NullInt32 `json:"pr_number"`
Environment string `json:"environment"`
Status string `json:"status"`
Url sql.NullString `json:"url"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
SvcID uuid.NullUUID `json:"svc_id"`
ServiceName sql.NullString `json:"service_name"`
ServiceType sql.NullString `json:"service_type"`
}
func (q *Queries) ListPreviewEnvironmentsForProject(ctx context.Context, projectID uuid.UUID) ([]ListPreviewEnvironmentsForProjectRow, error) {
rows, err := q.db.QueryContext(ctx, listPreviewEnvironmentsForProject, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListPreviewEnvironmentsForProjectRow{}
for rows.Next() {
var i ListPreviewEnvironmentsForProjectRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.PreviewServiceID,
&i.BranchName,
&i.PrNumber,
&i.Environment,
&i.Status,
&i.Url,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.SvcID,
&i.ServiceName,
&i.ServiceType,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listSweepablePreviews = `-- name: ListSweepablePreviews :many
SELECT id, preview_service_id FROM preview_environments
WHERE expires_at < NOW() AND status NOT IN ('expired', 'stopped')
`
type ListSweepablePreviewsRow struct {
ID uuid.UUID `json:"id"`
PreviewServiceID uuid.NullUUID `json:"preview_service_id"`
}
func (q *Queries) ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error) {
rows, err := q.db.QueryContext(ctx, listSweepablePreviews)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListSweepablePreviewsRow{}
for rows.Next() {
var i ListSweepablePreviewsRow
if err := rows.Scan(&i.ID, &i.PreviewServiceID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const markPreviewEnvironmentStatus = `-- name: MarkPreviewEnvironmentStatus :exec
UPDATE preview_environments SET status = $1, updated_at = NOW() WHERE id = $2
`
type MarkPreviewEnvironmentStatusParams struct {
Status string `json:"status"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) MarkPreviewEnvironmentStatus(ctx context.Context, arg MarkPreviewEnvironmentStatusParams) error {
_, err := q.db.ExecContext(ctx, markPreviewEnvironmentStatus, arg.Status, arg.ID)
return err
}
const syncPreviewStatuses = `-- name: SyncPreviewStatuses :exec
UPDATE preview_environments pe
SET status = derived.st, updated_at = NOW()
FROM (
SELECT pe2.id,
CASE
WHEN s.status IN ('running', 'deployed') THEN 'running'
WHEN s.status IN ('failed', 'error') THEN 'failed'
WHEN s.status IN ('building', 'deploying', 'queued', 'pending', 'cloning') THEN 'building'
ELSE pe2.status
END AS st
FROM preview_environments pe2
JOIN services s ON pe2.preview_service_id = s.id
) derived
WHERE pe.id = derived.id
AND pe.status IN ('building', 'running', 'failed')
AND pe.status <> derived.st
`
func (q *Queries) SyncPreviewStatuses(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, syncPreviewStatuses)
return err
}
const updatePreviewEnvironment = `-- name: UpdatePreviewEnvironment :exec
UPDATE preview_environments
SET status = $1, url = $2, expires_at = $3, updated_at = $4
WHERE id = $5
`
type UpdatePreviewEnvironmentParams struct {
Status string `json:"status"`
Url sql.NullString `json:"url"`
ExpiresAt time.Time `json:"expires_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdatePreviewEnvironment(ctx context.Context, arg UpdatePreviewEnvironmentParams) error {
_, err := q.db.ExecContext(ctx, updatePreviewEnvironment,
arg.Status,
arg.Url,
arg.ExpiresAt,
arg.UpdatedAt,
arg.ID,
)
return err
}
const updateServicePreviewBranch = `-- name: UpdateServicePreviewBranch :exec
UPDATE services SET git_branch = $1, domain = '', updated_at = NOW() WHERE id = $2
`
type UpdateServicePreviewBranchParams struct {
GitBranch sql.NullString `json:"git_branch"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateServicePreviewBranch(ctx context.Context, arg UpdateServicePreviewBranchParams) error {
_, err := q.db.ExecContext(ctx, updateServicePreviewBranch, arg.GitBranch, arg.ID)
return err
}
@@ -18,12 +18,20 @@ type Querier interface {
AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error) AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error)
ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error
ClearServiceNodePins(ctx context.Context, nodeID sql.NullString) (int64, error) ClearServiceNodePins(ctx context.Context, nodeID sql.NullString) (int64, error)
CloneServiceDomains(ctx context.Context, arg CloneServiceDomainsParams) error
CloneServiceRow(ctx context.Context, arg CloneServiceRowParams) error
CloneServiceVariables(ctx context.Context, arg CloneServiceVariablesParams) error
CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error) CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error)
CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error
CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error)
CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error)
CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error) CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error)
CountDatabasesUsingBackupTarget(ctx context.Context, targetID sql.NullString) (int64, error) CountDatabasesUsingBackupTarget(ctx context.Context, targetID sql.NullString) (int64, error)
CountDeploymentsForService(ctx context.Context, serviceID uuid.UUID) (int64, error)
CountEnvironmentServices(ctx context.Context, environmentID uuid.UUID) (int64, error) CountEnvironmentServices(ctx context.Context, environmentID uuid.UUID) (int64, error)
CountProjectsByUser(ctx context.Context, arg CountProjectsByUserParams) (int64, error) CountProjectsByUser(ctx context.Context, arg CountProjectsByUserParams) (int64, error)
CountServiceDomains(ctx context.Context, serviceID uuid.UUID) (int64, error) CountServiceDomains(ctx context.Context, serviceID uuid.UUID) (int64, error)
CountServicesByName(ctx context.Context, arg CountServicesByNameParams) (int64, error)
CountServicesByProjectAndName(ctx context.Context, arg CountServicesByProjectAndNameParams) (int64, error) CountServicesByProjectAndName(ctx context.Context, arg CountServicesByProjectAndNameParams) (int64, error)
CountUnreadNotificationsByUser(ctx context.Context, userID uuid.UUID) (int64, error) CountUnreadNotificationsByUser(ctx context.Context, userID uuid.UUID) (int64, error)
CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error) CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error)
@@ -37,6 +45,8 @@ type Querier interface {
CreateExternalDatabaseService(ctx context.Context, arg CreateExternalDatabaseServiceParams) error CreateExternalDatabaseService(ctx context.Context, arg CreateExternalDatabaseServiceParams) error
CreateNotification(ctx context.Context, arg CreateNotificationParams) error CreateNotification(ctx context.Context, arg CreateNotificationParams) error
CreateOutboundWebhook(ctx context.Context, arg CreateOutboundWebhookParams) error CreateOutboundWebhook(ctx context.Context, arg CreateOutboundWebhookParams) error
CreatePreviewDomain(ctx context.Context, arg CreatePreviewDomainParams) error
CreatePreviewEnvironment(ctx context.Context, arg CreatePreviewEnvironmentParams) error
CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error)
CreateServiceDomain(ctx context.Context, arg CreateServiceDomainParams) (CreateServiceDomainRow, error) CreateServiceDomain(ctx context.Context, arg CreateServiceDomainParams) (CreateServiceDomainRow, error)
CreateServiceFromTemplate(ctx context.Context, arg CreateServiceFromTemplateParams) error CreateServiceFromTemplate(ctx context.Context, arg CreateServiceFromTemplateParams) error
@@ -46,18 +56,22 @@ type Querier interface {
CreateWebhookDelivery(ctx context.Context, arg CreateWebhookDeliveryParams) error CreateWebhookDelivery(ctx context.Context, arg CreateWebhookDeliveryParams) error
DatabaseServiceExistsByIDAndUser(ctx context.Context, arg DatabaseServiceExistsByIDAndUserParams) (bool, error) DatabaseServiceExistsByIDAndUser(ctx context.Context, arg DatabaseServiceExistsByIDAndUserParams) (bool, error)
DeleteAgent(ctx context.Context, id string) error DeleteAgent(ctx context.Context, id string) error
DeleteAllServiceDomains(ctx context.Context, serviceID uuid.UUID) error
DeleteBackupTargetByIDAndUser(ctx context.Context, arg DeleteBackupTargetByIDAndUserParams) error DeleteBackupTargetByIDAndUser(ctx context.Context, arg DeleteBackupTargetByIDAndUserParams) error
DeleteBanner(ctx context.Context, id uuid.UUID) error DeleteBanner(ctx context.Context, id uuid.UUID) error
DeleteDatabaseServiceByID(ctx context.Context, id string) error DeleteDatabaseServiceByID(ctx context.Context, id string) error
DeleteDatabaseServiceByIDAndUser(ctx context.Context, arg DeleteDatabaseServiceByIDAndUserParams) error DeleteDatabaseServiceByIDAndUser(ctx context.Context, arg DeleteDatabaseServiceByIDAndUserParams) error
DeleteNotificationChannel(ctx context.Context, arg DeleteNotificationChannelParams) (int64, error) DeleteNotificationChannel(ctx context.Context, arg DeleteNotificationChannelParams) (int64, error)
DeleteOutboundWebhookByIDAndUser(ctx context.Context, arg DeleteOutboundWebhookByIDAndUserParams) error DeleteOutboundWebhookByIDAndUser(ctx context.Context, arg DeleteOutboundWebhookByIDAndUserParams) error
DeletePreviewEnvironment(ctx context.Context, id uuid.UUID) error
DeleteProjectByID(ctx context.Context, projectID uuid.UUID) (int64, error) DeleteProjectByID(ctx context.Context, projectID uuid.UUID) (int64, error)
DeleteProjectEnvironment(ctx context.Context, arg DeleteProjectEnvironmentParams) error DeleteProjectEnvironment(ctx context.Context, arg DeleteProjectEnvironmentParams) error
DeleteServiceByID(ctx context.Context, id uuid.UUID) error
DeleteServiceContainersOnAgent(ctx context.Context, arg DeleteServiceContainersOnAgentParams) error DeleteServiceContainersOnAgent(ctx context.Context, arg DeleteServiceContainersOnAgentParams) error
DeleteServiceDomain(ctx context.Context, arg DeleteServiceDomainParams) (int64, error) DeleteServiceDomain(ctx context.Context, arg DeleteServiceDomainParams) (int64, error)
DeleteUserInvite(ctx context.Context, id uuid.UUID) error DeleteUserInvite(ctx context.Context, id uuid.UUID) error
DeleteUserTemplate(ctx context.Context, arg DeleteUserTemplateParams) (int64, error) DeleteUserTemplate(ctx context.Context, arg DeleteUserTemplateParams) (int64, error)
FailDeployment(ctx context.Context, arg FailDeploymentParams) error
GetActiveAgentAuthTokenByHash(ctx context.Context, tokenHash string) (AgentAuthToken, error) GetActiveAgentAuthTokenByHash(ctx context.Context, tokenHash string) (AgentAuthToken, error)
GetAgent(ctx context.Context, id string) (NodeAgent, error) GetAgent(ctx context.Context, id string) (NodeAgent, error)
GetAgentByHostAndIP(ctx context.Context, arg GetAgentByHostAndIPParams) (NodeAgent, error) GetAgentByHostAndIP(ctx context.Context, arg GetAgentByHostAndIPParams) (NodeAgent, error)
@@ -71,23 +85,40 @@ type Querier interface {
GetDatabaseServiceByID(ctx context.Context, id string) (DatabaseService, error) GetDatabaseServiceByID(ctx context.Context, id string) (DatabaseService, error)
GetDatabaseServiceByIDAndUser(ctx context.Context, arg GetDatabaseServiceByIDAndUserParams) (DatabaseService, error) GetDatabaseServiceByIDAndUser(ctx context.Context, arg GetDatabaseServiceByIDAndUserParams) (DatabaseService, error)
GetDatabaseServiceOwnerID(ctx context.Context, id string) (string, error) GetDatabaseServiceOwnerID(ctx context.Context, id string) (string, error)
GetDeploymentAccess(ctx context.Context, id uuid.UUID) (GetDeploymentAccessRow, error)
GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error)
GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error)
GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error) GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error)
GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error) GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error)
GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error) GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error)
GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error)
GetPreviewEnvironmentForPromote(ctx context.Context, arg GetPreviewEnvironmentForPromoteParams) (GetPreviewEnvironmentForPromoteRow, error)
GetPreviewEnvironmentForWrite(ctx context.Context, arg GetPreviewEnvironmentForWriteParams) (GetPreviewEnvironmentForWriteRow, error)
GetPreviewEnvironmentOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetPreviewServiceID(ctx context.Context, id uuid.UUID) (uuid.NullUUID, error)
GetProjectBrief(ctx context.Context, id uuid.UUID) (GetProjectBriefRow, error)
GetProjectByIDForUser(ctx context.Context, arg GetProjectByIDForUserParams) (Project, error) GetProjectByIDForUser(ctx context.Context, arg GetProjectByIDForUserParams) (Project, error)
GetProjectEnvironmentByName(ctx context.Context, arg GetProjectEnvironmentByNameParams) (Environment, error) GetProjectEnvironmentByName(ctx context.Context, arg GetProjectEnvironmentByNameParams) (Environment, error)
GetProjectOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetProjectOwnerByID(ctx context.Context, projectID uuid.UUID) (uuid.UUID, error) GetProjectOwnerByID(ctx context.Context, projectID uuid.UUID) (uuid.UUID, error)
GetProjectOwnerID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) GetProjectOwnerID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error) GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error)
GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error) GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error)
GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error) GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error)
GetServiceDomainFirst(ctx context.Context, serviceID uuid.UUID) (string, error) GetServiceDomainFirst(ctx context.Context, serviceID uuid.UUID) (string, error)
GetServiceEnvironment(ctx context.Context, id uuid.UUID) (string, error)
GetServiceForDeploy(ctx context.Context, id uuid.UUID) (GetServiceForDeployRow, error)
GetServiceForDeployWithOwner(ctx context.Context, id uuid.UUID) (GetServiceForDeployWithOwnerRow, error)
GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error) GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error)
GetServicePublishedPort(ctx context.Context, id uuid.UUID) (int32, error)
GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error) GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error)
GetServiceTypeBrief(ctx context.Context, arg GetServiceTypeBriefParams) (GetServiceTypeBriefRow, error)
GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error) GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error)
GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error) GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error)
InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error
InsertDeployment(ctx context.Context, arg InsertDeploymentParams) error
InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error) InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error)
InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error
InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error
ListActiveBanners(ctx context.Context) ([]Banner, error) ListActiveBanners(ctx context.Context) ([]Banner, error)
ListAgentAuthTokens(ctx context.Context) ([]AgentAuthToken, error) ListAgentAuthTokens(ctx context.Context) ([]AgentAuthToken, error)
@@ -102,8 +133,10 @@ type Querier interface {
ListDatabaseBackupsByDatabaseAndUser(ctx context.Context, arg ListDatabaseBackupsByDatabaseAndUserParams) ([]DatabaseBackup, error) ListDatabaseBackupsByDatabaseAndUser(ctx context.Context, arg ListDatabaseBackupsByDatabaseAndUserParams) ([]DatabaseBackup, error)
ListDatabaseServicesByProject(ctx context.Context, projectID uuid.NullUUID) ([]DatabaseService, error) ListDatabaseServicesByProject(ctx context.Context, projectID uuid.NullUUID) ([]DatabaseService, error)
ListDatabaseServicesByUser(ctx context.Context, userID string) ([]DatabaseService, error) ListDatabaseServicesByUser(ctx context.Context, userID string) ([]DatabaseService, error)
ListDeploymentsForService(ctx context.Context, arg ListDeploymentsForServiceParams) ([]ListDeploymentsForServiceRow, error)
ListDueDatabaseBackups(ctx context.Context) ([]ListDueDatabaseBackupsRow, error) ListDueDatabaseBackups(ctx context.Context) ([]ListDueDatabaseBackupsRow, error)
ListEnabledOutboundWebhooks(ctx context.Context) ([]OutboundWebhook, error) ListEnabledOutboundWebhooks(ctx context.Context) ([]OutboundWebhook, error)
ListExpiredPreviewsForUser(ctx context.Context, arg ListExpiredPreviewsForUserParams) ([]ListExpiredPreviewsForUserRow, error)
ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error) ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error)
ListNodeMetrics(ctx context.Context, arg ListNodeMetricsParams) ([]ListNodeMetricsRow, error) ListNodeMetrics(ctx context.Context, arg ListNodeMetricsParams) ([]ListNodeMetricsRow, error)
ListNotificationChannelsByUser(ctx context.Context, userID uuid.UUID) ([]NotificationChannel, error) ListNotificationChannelsByUser(ctx context.Context, userID uuid.UUID) ([]NotificationChannel, error)
@@ -111,11 +144,13 @@ type Querier interface {
ListOutboundWebhooksByUser(ctx context.Context, userID uuid.UUID) ([]OutboundWebhook, error) ListOutboundWebhooksByUser(ctx context.Context, userID uuid.UUID) ([]OutboundWebhook, error)
ListPendingCommands(ctx context.Context, nodeAgentID string) ([]AgentCommand, error) ListPendingCommands(ctx context.Context, nodeAgentID string) ([]AgentCommand, error)
ListPendingProjects(ctx context.Context) ([]Project, error) ListPendingProjects(ctx context.Context) ([]Project, error)
ListPreviewEnvironmentsForProject(ctx context.Context, projectID uuid.UUID) ([]ListPreviewEnvironmentsForProjectRow, error)
ListProjectEnvironments(ctx context.Context, projectID uuid.UUID) ([]ListProjectEnvironmentsRow, error) ListProjectEnvironments(ctx context.Context, projectID uuid.UUID) ([]ListProjectEnvironmentsRow, error)
// Visibility model: a project is readable when it is approved for public // Visibility model: a project is readable when it is approved for public
// display, when the caller owns or is a member of it, or when the caller is a // display, when the caller owns or is a member of it, or when the caller is a
// platform admin. Anonymous callers pass user_id = uuid.Nil, is_admin = false. // platform admin. Anonymous callers pass user_id = uuid.Nil, is_admin = false.
ListProjectsWithStatsByUser(ctx context.Context, arg ListProjectsWithStatsByUserParams) ([]ListProjectsWithStatsByUserRow, error) ListProjectsWithStatsByUser(ctx context.Context, arg ListProjectsWithStatsByUserParams) ([]ListProjectsWithStatsByUserRow, error)
ListRecentAccessibleDeployments(ctx context.Context, arg ListRecentAccessibleDeploymentsParams) ([]ListRecentAccessibleDeploymentsRow, error)
// Base domains of every online, schedulable agent carrying the required // Base domains of every online, schedulable agent carrying the required
// placement tags — auto-domain candidates for spread services. // placement tags — auto-domain candidates for spread services.
ListSchedulableAgentDomains(ctx context.Context, dollar_1 json.RawMessage) ([]string, error) ListSchedulableAgentDomains(ctx context.Context, dollar_1 json.RawMessage) ([]string, error)
@@ -129,12 +164,15 @@ type Querier interface {
ListServiceMetrics(ctx context.Context, arg ListServiceMetricsParams) ([]ListServiceMetricsRow, error) ListServiceMetrics(ctx context.Context, arg ListServiceMetricsParams) ([]ListServiceMetricsRow, error)
ListServiceTemplatesByCategoryForUser(ctx context.Context, arg ListServiceTemplatesByCategoryForUserParams) ([]ServiceTemplate, error) ListServiceTemplatesByCategoryForUser(ctx context.Context, arg ListServiceTemplatesByCategoryForUserParams) ([]ServiceTemplate, error)
ListServiceTemplatesForUser(ctx context.Context, ownerID uuid.NullUUID) ([]ServiceTemplate, error) ListServiceTemplatesForUser(ctx context.Context, ownerID uuid.NullUUID) ([]ServiceTemplate, error)
ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error)
ListUserInvites(ctx context.Context) ([]UserInvite, error) ListUserInvites(ctx context.Context) ([]UserInvite, error)
ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error) ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error)
ListWebhookDeliveriesByWebhook(ctx context.Context, webhookID uuid.UUID) ([]WebhookDelivery, error) ListWebhookDeliveriesByWebhook(ctx context.Context, webhookID uuid.UUID) ([]WebhookDelivery, error)
MarkAllNotificationsReadByUser(ctx context.Context, arg MarkAllNotificationsReadByUserParams) error MarkAllNotificationsReadByUser(ctx context.Context, arg MarkAllNotificationsReadByUserParams) error
MarkNotificationReadByIDAndUser(ctx context.Context, arg MarkNotificationReadByIDAndUserParams) error MarkNotificationReadByIDAndUser(ctx context.Context, arg MarkNotificationReadByIDAndUserParams) error
MarkPreviewEnvironmentStatus(ctx context.Context, arg MarkPreviewEnvironmentStatusParams) error
MarkUserInviteUsed(ctx context.Context, arg MarkUserInviteUsedParams) error MarkUserInviteUsed(ctx context.Context, arg MarkUserInviteUsedParams) error
MoveServiceProject(ctx context.Context, arg MoveServiceProjectParams) (int64, error)
// Resource-aware: lowest memory utilisation first, then cpu, then container // Resource-aware: lowest memory utilisation first, then cpu, then container
// count. Agents without telemetry sort last (NULL → worst score). // count. Agents without telemetry sort last (NULL → worst score).
PickLeastLoadedAgent(ctx context.Context) (string, error) PickLeastLoadedAgent(ctx context.Context) (string, error)
@@ -157,9 +195,13 @@ type Querier interface {
SetDatabaseServiceStatusAndConnectionByID(ctx context.Context, arg SetDatabaseServiceStatusAndConnectionByIDParams) error SetDatabaseServiceStatusAndConnectionByID(ctx context.Context, arg SetDatabaseServiceStatusAndConnectionByIDParams) error
SetDatabaseServiceStatusByID(ctx context.Context, arg SetDatabaseServiceStatusByIDParams) error SetDatabaseServiceStatusByID(ctx context.Context, arg SetDatabaseServiceStatusByIDParams) error
SetDatabaseServiceStatusByIDAndUser(ctx context.Context, arg SetDatabaseServiceStatusByIDAndUserParams) error SetDatabaseServiceStatusByIDAndUser(ctx context.Context, arg SetDatabaseServiceStatusByIDAndUserParams) error
SetDeploymentStatus(ctx context.Context, arg SetDeploymentStatusParams) error
SetProjectApproved(ctx context.Context, arg SetProjectApprovedParams) (int64, error) SetProjectApproved(ctx context.Context, arg SetProjectApprovedParams) (int64, error)
SetServiceDomainDefault(ctx context.Context, arg SetServiceDomainDefaultParams) (int64, error) SetServiceDomainDefault(ctx context.Context, arg SetServiceDomainDefaultParams) (int64, error)
SetServiceLegacyDomain(ctx context.Context, arg SetServiceLegacyDomainParams) error SetServiceLegacyDomain(ctx context.Context, arg SetServiceLegacyDomainParams) error
SetServiceStatus(ctx context.Context, arg SetServiceStatusParams) error
SyncDeploymentProgress(ctx context.Context, arg SyncDeploymentProgressParams) error
SyncPreviewStatuses(ctx context.Context) error
TouchAgentAuthToken(ctx context.Context, tokenHash string) error TouchAgentAuthToken(ctx context.Context, tokenHash string) error
TouchServiceDomainChecked(ctx context.Context, id uuid.UUID) error TouchServiceDomainChecked(ctx context.Context, id uuid.UUID) error
TouchUserToken(ctx context.Context, id uuid.UUID) error TouchUserToken(ctx context.Context, id uuid.UUID) error
@@ -179,7 +221,9 @@ type Querier interface {
UpdateDatabaseServiceNameByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameByIDAndUserParams) error UpdateDatabaseServiceNameByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameByIDAndUserParams) error
UpdateDatabaseServicePlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServicePlanByIDAndUserParams) error UpdateDatabaseServicePlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServicePlanByIDAndUserParams) error
UpdateOutboundWebhook(ctx context.Context, arg UpdateOutboundWebhookParams) error UpdateOutboundWebhook(ctx context.Context, arg UpdateOutboundWebhookParams) error
UpdatePreviewEnvironment(ctx context.Context, arg UpdatePreviewEnvironmentParams) error
UpdateProjectByID(ctx context.Context, arg UpdateProjectByIDParams) (int64, error) UpdateProjectByID(ctx context.Context, arg UpdateProjectByIDParams) (int64, error)
UpdateServicePreviewBranch(ctx context.Context, arg UpdateServicePreviewBranchParams) error
UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error) UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error)
UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error
UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error
@@ -0,0 +1,158 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: service_ops.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const cloneServiceDomains = `-- name: CloneServiceDomains :exec
INSERT INTO service_domains (service_id, domain, is_default, cert_type)
SELECT $1, domain, is_default, cert_type FROM service_domains sd WHERE sd.service_id = $2
`
type CloneServiceDomainsParams struct {
ServiceID uuid.UUID `json:"service_id"`
ServiceID_2 uuid.UUID `json:"service_id_2"`
}
func (q *Queries) CloneServiceDomains(ctx context.Context, arg CloneServiceDomainsParams) error {
_, err := q.db.ExecContext(ctx, cloneServiceDomains, arg.ServiceID, arg.ServiceID_2)
return err
}
const cloneServiceRow = `-- name: CloneServiceRow :exec
INSERT INTO services
(id, project_id, name, environment_id, service_type, source_type, source_url, image_name,
build_command, start_command, type, status, image, command, environment,
git_repo, git_branch, build_path, cpu, memory, replicas, port, domain,
healthcheck_path, restart_policy, volumes, maintenance_mode, basic_auth_users,
builder, cpu_reserve, memory_reserve, static_build_cmd, static_dir,
traefik_labels, created_at, updated_at)
SELECT $1, $2, $3, $4, service_type, source_type, source_url, image_name,
build_command, start_command, type, 'stopped', image, command, $5,
git_repo, git_branch, build_path, cpu, memory, replicas, port, domain,
healthcheck_path, restart_policy, volumes, $6, $7,
builder, cpu_reserve, memory_reserve, static_build_cmd, static_dir,
traefik_labels, $8, $8
FROM services s WHERE s.id = $9
`
type CloneServiceRowParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
EnvironmentID uuid.UUID `json:"environment_id"`
Environment sql.NullString `json:"environment"`
MaintenanceMode bool `json:"maintenance_mode"`
BasicAuthUsers string `json:"basic_auth_users"`
CreatedAt sql.NullTime `json:"created_at"`
ID_2 uuid.UUID `json:"id_2"`
}
func (q *Queries) CloneServiceRow(ctx context.Context, arg CloneServiceRowParams) error {
_, err := q.db.ExecContext(ctx, cloneServiceRow,
arg.ID,
arg.ProjectID,
arg.Name,
arg.EnvironmentID,
arg.Environment,
arg.MaintenanceMode,
arg.BasicAuthUsers,
arg.CreatedAt,
arg.ID_2,
)
return err
}
const cloneServiceVariables = `-- name: CloneServiceVariables :exec
INSERT INTO environment_variables (id, service_id, key, value, is_secret)
SELECT gen_random_uuid(), $1, key, value, is_secret FROM environment_variables ev WHERE ev.service_id = $2
`
type CloneServiceVariablesParams struct {
ServiceID uuid.UUID `json:"service_id"`
ServiceID_2 uuid.UUID `json:"service_id_2"`
}
func (q *Queries) CloneServiceVariables(ctx context.Context, arg CloneServiceVariablesParams) error {
_, err := q.db.ExecContext(ctx, cloneServiceVariables, arg.ServiceID, arg.ServiceID_2)
return err
}
const countServicesByName = `-- name: CountServicesByName :one
SELECT COUNT(*) FROM services WHERE project_id = $1 AND name = $2
`
type CountServicesByNameParams struct {
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
}
func (q *Queries) CountServicesByName(ctx context.Context, arg CountServicesByNameParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countServicesByName, arg.ProjectID, arg.Name)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteServiceByID = `-- name: DeleteServiceByID :exec
DELETE FROM services WHERE id = $1
`
func (q *Queries) DeleteServiceByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteServiceByID, id)
return err
}
const getProjectOwner = `-- name: GetProjectOwner :one
SELECT owner_id FROM projects WHERE id = $1
`
func (q *Queries) GetProjectOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getProjectOwner, id)
var owner_id uuid.UUID
err := row.Scan(&owner_id)
return owner_id, err
}
const getServiceEnvironment = `-- name: GetServiceEnvironment :one
SELECT COALESCE(environment, '') FROM services WHERE id = $1
`
func (q *Queries) GetServiceEnvironment(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceEnvironment, id)
var environment string
err := row.Scan(&environment)
return environment, err
}
const moveServiceProject = `-- name: MoveServiceProject :execrows
UPDATE services SET project_id = $1, environment_id = $2, updated_at = $3 WHERE id = $4
`
type MoveServiceProjectParams struct {
ProjectID uuid.UUID `json:"project_id"`
EnvironmentID uuid.UUID `json:"environment_id"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) MoveServiceProject(ctx context.Context, arg MoveServiceProjectParams) (int64, error) {
result, err := q.db.ExecContext(ctx, moveServiceProject,
arg.ProjectID,
arg.EnvironmentID,
arg.UpdatedAt,
arg.ID,
)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
+105
View File
@@ -0,0 +1,105 @@
-- name: CountDeploymentsForService :one
SELECT COUNT(*) FROM deployments WHERE service_id = $1;
-- name: ListDeploymentsForService :many
SELECT id, service_id, commit_hash, status, image_name, image_tag,
build_log, runtime_log, error, started_at, completed_at, created_at, updated_at
FROM deployments
WHERE service_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
-- name: CountAccessibleDeployments :one
SELECT COUNT(*)
FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1);
-- name: ListRecentAccessibleDeployments :many
SELECT d.id, d.service_id, s.name AS service_name, p.name AS project_name, d.status,
COALESCE(d.image_name, '') AS image_name, d.started_at, d.completed_at, d.created_at
FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)
ORDER BY d.created_at DESC
LIMIT $3 OFFSET $4;
-- name: GetServiceForDeployWithOwner :one
SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1) AS replicas, COALESCE(s.port, 0) AS port,
COALESCE(s.domain, '') AS domain, COALESCE(s.healthcheck_path, '') AS healthcheck_path,
COALESCE(s.restart_policy, 'unless-stopped') AS restart_policy,
COALESCE(s.builder, 'auto') AS builder, COALESCE(s.cpu_reserve, '') AS cpu_reserve,
COALESCE(s.memory_reserve, '') AS memory_reserve, COALESCE(s.static_build_cmd, '') AS static_build_cmd,
COALESCE(s.static_dir, '') AS static_dir,
s.created_at, s.updated_at, p.owner_id
FROM services s
JOIN projects p ON s.project_id = p.id
WHERE s.id = $1;
-- name: InsertDeployment :exec
INSERT INTO deployments
(id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
-- name: FailDeployment :exec
UPDATE deployments
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
WHERE id = $3;
-- name: CompleteDeployment :exec
UPDATE deployments
SET status = $1, error = $2, completed_at = $3, updated_at = $3
WHERE id = $4;
-- name: SetDeploymentStatus :exec
UPDATE deployments SET status = $1, updated_at = $2 WHERE id = $3;
-- name: SetServiceStatus :exec
UPDATE services SET status = $1, updated_at = $2 WHERE id = $3;
-- name: SyncDeploymentProgress :exec
UPDATE deployments
SET status = $1,
image_name = $2,
image_tag = $3,
build_log = $4,
runtime_log = $5,
error = $6,
started_at = $7,
completed_at = $8,
updated_at = $9
WHERE id = $10;
-- name: GetDeploymentAccess :one
SELECT d.status, d.service_id, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1;
-- name: GetDeploymentWithProject :one
SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.id AS project_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1;
-- name: GetDeploymentForRollback :one
SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1;
-- name: GetServicePublishedPort :one
SELECT published_port FROM services WHERE id = $1;
@@ -0,0 +1,121 @@
-- name: ListPreviewEnvironmentsForProject :many
SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 svc_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;
-- name: GetPreviewEnvironment :one
SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 svc_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.is_approved OR p.owner_id = $2 OR $3::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $2));
-- name: GetPreviewEnvironmentForWrite :one
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 OR $3::bool);
-- name: GetPreviewEnvironmentForPromote :one
SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 OR $3::bool);
-- name: GetPreviewEnvironmentOwner :one
SELECT p.owner_id
FROM preview_environments pe
JOIN projects p ON pe.project_id = p.id
WHERE pe.id = $1;
-- name: GetPreviewServiceID :one
SELECT preview_service_id FROM preview_environments WHERE id = $1;
-- name: UpdatePreviewEnvironment :exec
UPDATE preview_environments
SET status = $1, url = $2, expires_at = $3, updated_at = $4
WHERE id = $5;
-- name: MarkPreviewEnvironmentStatus :exec
UPDATE preview_environments SET status = $1, updated_at = NOW() WHERE id = $2;
-- name: DeletePreviewEnvironment :exec
DELETE FROM preview_environments WHERE id = $1;
-- name: CountActivePreviewsForBranch :one
SELECT COUNT(*) FROM preview_environments
WHERE service_id = $1 AND branch_name = $2 AND status NOT IN ('expired', 'stopped');
-- name: CreatePreviewEnvironment :exec
INSERT INTO preview_environments
(id, project_id, service_id, preview_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, $12);
-- name: UpdateServicePreviewBranch :exec
UPDATE services SET git_branch = $1, domain = '', updated_at = NOW() WHERE id = $2;
-- name: DeleteAllServiceDomains :exec
DELETE FROM service_domains WHERE service_id = $1;
-- name: CreatePreviewDomain :exec
INSERT INTO service_domains (service_id, domain, is_default, cert_type) VALUES ($1, $2, true, 'tls');
-- name: InsertPendingDeployment :exec
INSERT INTO deployments (id, service_id, version, commit_hash, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'pending', NOW(), NOW());
-- name: ListExpiredPreviewsForUser :many
SELECT pe.id, pe.project_id, pe.service_id, pe.preview_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 OR $2::bool) AND pe.expires_at < NOW() AND pe.status != 'expired';
-- name: ListSweepablePreviews :many
SELECT id, preview_service_id FROM preview_environments
WHERE expires_at < NOW() AND status NOT IN ('expired', 'stopped');
-- name: SyncPreviewStatuses :exec
UPDATE preview_environments pe
SET status = derived.st, updated_at = NOW()
FROM (
SELECT pe2.id,
CASE
WHEN s.status IN ('running', 'deployed') THEN 'running'
WHEN s.status IN ('failed', 'error') THEN 'failed'
WHEN s.status IN ('building', 'deploying', 'queued', 'pending', 'cloning') THEN 'building'
ELSE pe2.status
END AS st
FROM preview_environments pe2
JOIN services s ON pe2.preview_service_id = s.id
) derived
WHERE pe.id = derived.id
AND pe.status IN ('building', 'running', 'failed')
AND pe.status <> derived.st;
-- name: GetProjectBrief :one
SELECT id, name, owner_id FROM projects WHERE id = $1;
-- name: GetServiceTypeBrief :one
SELECT id, name, COALESCE(type, service_type, '') AS service_type FROM services WHERE id = $1 AND project_id = $2;
-- name: GetServiceForDeploy :one
SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1) AS replicas, COALESCE(s.port, 0) AS port,
COALESCE(s.domain, '') AS domain, COALESCE(s.healthcheck_path, '') AS healthcheck_path,
COALESCE(s.restart_policy, 'unless-stopped') AS restart_policy,
COALESCE(s.builder, 'auto') AS builder, COALESCE(s.cpu_reserve, '') AS cpu_reserve,
COALESCE(s.memory_reserve, '') AS memory_reserve, COALESCE(s.static_build_cmd, '') AS static_build_cmd,
COALESCE(s.static_dir, '') AS static_dir,
s.created_at, s.updated_at
FROM services s
WHERE s.id = $1;
+38
View File
@@ -0,0 +1,38 @@
-- name: CountServicesByName :one
SELECT COUNT(*) FROM services WHERE project_id = $1 AND name = $2;
-- name: GetServiceEnvironment :one
SELECT COALESCE(environment, '') FROM services WHERE id = $1;
-- name: CloneServiceRow :exec
INSERT INTO services
(id, project_id, name, environment_id, service_type, source_type, source_url, image_name,
build_command, start_command, type, status, image, command, environment,
git_repo, git_branch, build_path, cpu, memory, replicas, port, domain,
healthcheck_path, restart_policy, volumes, maintenance_mode, basic_auth_users,
builder, cpu_reserve, memory_reserve, static_build_cmd, static_dir,
traefik_labels, created_at, updated_at)
SELECT $1, $2, $3, $4, service_type, source_type, source_url, image_name,
build_command, start_command, type, 'stopped', image, command, $5,
git_repo, git_branch, build_path, cpu, memory, replicas, port, domain,
healthcheck_path, restart_policy, volumes, $6, $7,
builder, cpu_reserve, memory_reserve, static_build_cmd, static_dir,
traefik_labels, $8, $8
FROM services s WHERE s.id = $9;
-- name: CloneServiceDomains :exec
INSERT INTO service_domains (service_id, domain, is_default, cert_type)
SELECT $1, domain, is_default, cert_type FROM service_domains sd WHERE sd.service_id = $2;
-- name: CloneServiceVariables :exec
INSERT INTO environment_variables (id, service_id, key, value, is_secret)
SELECT gen_random_uuid(), $1, key, value, is_secret FROM environment_variables ev WHERE ev.service_id = $2;
-- name: DeleteServiceByID :exec
DELETE FROM services WHERE id = $1;
-- name: MoveServiceProject :execrows
UPDATE services SET project_id = $1, environment_id = $2, updated_at = $3 WHERE id = $4;
-- name: GetProjectOwner :one
SELECT owner_id FROM projects WHERE id = $1;
+29 -3
View File
@@ -98,11 +98,37 @@ CREATE TABLE service_domains (
); );
CREATE TABLE deployments ( CREATE TABLE deployments (
id UUID PRIMARY KEY, id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
service_id UUID NOT NULL REFERENCES services(id) ON DELETE CASCADE, service_id UUID NOT NULL REFERENCES services(id) ON DELETE CASCADE,
status VARCHAR(50), version VARCHAR(255) NOT NULL,
commit_hash VARCHAR(255),
image_digest VARCHAR(255),
status VARCHAR(50) DEFAULT 'created',
build_log TEXT,
deployment_log TEXT,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
image_name VARCHAR(255),
image_tag VARCHAR(255),
runtime_log TEXT,
error TEXT
);
CREATE TABLE preview_environments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL,
service_id UUID NOT NULL,
branch_name VARCHAR(255) NOT NULL,
pr_number INTEGER,
environment VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'building',
url TEXT,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
preview_service_id UUID
); );
CREATE TABLE environment_variables ( CREATE TABLE environment_variables (