Compare commits

..
Author SHA1 Message Date
Tomas DvorakandDevin b7191ad834 feat(templates): visual graph builder
Canvas mode in the template editor (lazy-loaded React Flow): service
nodes for web/worker/cron/database, connect handles to set depends_on,
per-node inspector (image/repo, engine, port, health check, env,
volumes), live validation (dup keys, missing deps, cycles, missing
source), and an Apply step that serializes back to the version-2 graph
config JSON.

- builder-model.ts is pure: config↔canvas round-trip, layered
  auto-layout, flat-legacy→single-node migration, validation —
  16 vitest cases cover all of it
- node positions persist under config.canvas (backend ignores unknown
  config keys, so no schema change)
- flat legacy templates open as a single node so nothing is lost on
  edit

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-10 02:45:45 +02:00
41 changed files with 1864 additions and 8463 deletions

No files matched your search

+1 -1
View File
@@ -537,7 +537,7 @@ and wired refs, one click or one CLI call.
workspace, CLI `environments` + MCP parity. Per-env vars/domains ride the workspace, CLI `environments` + MCP parity. Per-env vars/domains ride the
service's own scope (one env per service); promote ships via preview service's own scope (one env per service); promote ships via preview
promote. promote.
- [ ] **sqlc port** — ~265 raw `database/sql` sites → `sqlc/queries/`. Done in tranches (dynamic queries stay raw). Tranches merged: metrics storage + service domains (#75), service clone/move ops (#76), preview environments (#77), deployments + rollback (#78). Remaining: runtime, compliance, cron, git, security, auth, sleeper, scanner, gateway, and smaller files. - [ ] **sqlc port** — ~140 raw `database/sql` sites → `sqlc/queries/`.
- [ ] **Tests** — canvas/wizard frontend coverage, deployment/template/ - [ ] **Tests** — canvas/wizard frontend coverage, deployment/template/
webhook table tests, Playwright e2e golden path. webhook table tests, Playwright e2e golden path.
- [x] **First-run wizard** — public `GET /setup/status` + authed - [x] **First-run wizard** — public `GET /setup/status` + authed
+46 -78
View File
@@ -3,8 +3,6 @@ package api
import ( import (
"bytes" "bytes"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -65,16 +63,11 @@ func handleLogin(c *gin.Context) {
// Find user by email // Find user by email
var user User var user User
var hashedPassword string var hashedPassword string
urow, err := sqlcdb.New(db.DB).GetUserByEmailForAuth(context.Background(), req.Email) err := db.QueryRow(`
user = User{ SELECT id, email, password_hash, name, COALESCE(avatar_url, ''), is_admin, created_at
ID: urow.ID.String(), FROM users
Email: urow.Email, WHERE email = $1
Name: urow.Name, `, req.Email).Scan(&user.ID, &user.Email, &hashedPassword, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
hashedPassword = urow.PasswordHash
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
authUser, ok := verifyBetterAuthCredentials(c, req.Email, req.Password) authUser, ok := verifyBetterAuthCredentials(c, req.Email, req.Password)
@@ -264,30 +257,25 @@ func createLocalUserFromBetterAuth(db *database.DB, authUser betterAuthLoginUser
} }
// A mirrored first account owns the platform, same as handleRegister. // A mirrored first account owns the platform, same as handleRegister.
q := sqlcdb.New(db.DB) var total int
total64, err := q.CountUsers(context.Background()) if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&total); err != nil {
if err != nil {
return User{}, err return User{}, err
} }
urow, err := q.UpsertLocalUser(context.Background(), sqlcdb.UpsertLocalUserParams{ var user User
Email: email, err = db.QueryRow(`
PasswordHash: string(hashedPassword), INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
Name: name, VALUES ($1, $2, $3, NULLIF($4, ''), $5)
Column4: strings.TrimSpace(authUser.Image), ON CONFLICT (email) DO UPDATE
IsAdmin: total64 == 0, SET name = EXCLUDED.name,
}) avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = NOW()
RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
`, email, string(hashedPassword), name, strings.TrimSpace(authUser.Image), total == 0).
Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
if err != nil { if err != nil {
return User{}, err return User{}, err
} }
user := User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
return user, nil return user, nil
} }
@@ -314,8 +302,8 @@ func handleRegister(c *gin.Context) {
return return
} }
q := sqlcdb.New(db.DB) var existing int
existing, err := q.CountUsersByEmail(context.Background(), strings.ToLower(strings.TrimSpace(req.Email))) err = db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", strings.ToLower(strings.TrimSpace(req.Email))).Scan(&existing)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
@@ -334,20 +322,12 @@ func handleRegister(c *gin.Context) {
} }
// First account owns the platform. // First account owns the platform.
urow, err := q.InsertUserAdmin(context.Background(), sqlcdb.InsertUserAdminParams{ var user User
Email: req.Email, err = db.QueryRow(`
PasswordHash: string(hashedPassword), INSERT INTO users (email, password_hash, name, is_admin)
Name: req.Name, VALUES ($1, $2, $3, $4)
IsAdmin: total == 0, RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
}) `, req.Email, string(hashedPassword), req.Name, total == 0).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
user := User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
@@ -385,8 +365,8 @@ func handleCreateUser(c *gin.Context) {
return return
} }
existing, err := sqlcdb.New(db.DB).CountUsersByEmail(context.Background(), req.Email) var existing int
if err != nil { if err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", req.Email).Scan(&existing); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
@@ -410,19 +390,12 @@ func handleCreateUser(c *gin.Context) {
return return
} }
urow, err := sqlcdb.New(db.DB).InsertUser(context.Background(), sqlcdb.InsertUserParams{ var user User
Email: req.Email, err = db.QueryRow(`
PasswordHash: string(hashedPassword), INSERT INTO users (email, password_hash, name)
Name: req.Name, VALUES ($1, $2, $3)
}) RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
user := User{ `, req.Email, string(hashedPassword), req.Name).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err != nil { if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "User already exists"}) c.JSON(http.StatusConflict, gin.H{"error": "User already exists"})
return return
@@ -485,11 +458,10 @@ func createBetterAuthUser(c *gin.Context, name string, email string, password st
} }
func countLocalUsers(db *database.DB) (int, error) { func countLocalUsers(db *database.DB) (int, error) {
count64, err := sqlcdb.New(db.DB).CountUsers(context.Background()) var count int
if err != nil { if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
return 0, err return 0, err
} }
count := int(count64)
// Better Auth writes to auth_users; a row there only mirrors into users on // Better Auth writes to auth_users; a row there only mirrors into users on
// the first authenticated call. Count it too or a second public sign-up // the first authenticated call. Count it too or a second public sign-up
// slips through the bootstrap window. UNION by email avoids double-counting // slips through the bootstrap window. UNION by email avoids double-counting
@@ -517,15 +489,11 @@ func handleGetProfile(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
var user User var user User
urow, err := sqlcdb.New(db.DB).GetUserByID(context.Background(), uuid.MustParse(userID)) err := db.QueryRow(`
user = User{ SELECT id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
ID: urow.ID.String(), FROM users
Email: urow.Email, WHERE id = $1
Name: urow.Name, `, userID).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
@@ -553,11 +521,11 @@ func handleUpdateProfile(c *gin.Context) {
} }
// Update user profile // Update user profile
err := sqlcdb.New(db.DB).UpdateUserProfile(context.Background(), sqlcdb.UpdateUserProfileParams{ _, err := db.Exec(`
Column1: req.Name, UPDATE users
Column2: req.AvatarURL, SET name = COALESCE($1, name), avatar_url = COALESCE($2, avatar_url)
ID: uuid.MustParse(userID), WHERE id = $3
}) `, req.Name, req.AvatarURL, userID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"})
+123 -147
View File
@@ -65,47 +65,13 @@ type UpdateCronJobRequest struct {
Retention int `json:"retention"` Retention int `json:"retention"`
} }
// cronJobFrom maps the sqlc row onto the string-ID API model.
func cronJobFrom(j sqlcdb.CronJob) CronJob {
return CronJob{
ID: j.ID.String(),
ProjectID: j.ProjectID.String(),
ServiceID: j.ServiceID.String(),
Name: j.Name,
Schedule: j.Schedule,
Command: j.Command,
Timezone: j.Timezone.String,
Enabled: j.Enabled.Bool,
LastRunAt: timePtrNT(j.LastRunAt),
NextRunAt: timePtrNT(j.NextRunAt),
LastStatus: j.LastStatus.String,
LastOutput: j.LastOutput.String,
Retention: int(j.Retention.Int32),
CreatedAt: j.CreatedAt.Time,
UpdatedAt: j.UpdatedAt.Time,
}
}
// cronExecutionFrom maps the sqlc row onto the string-ID API model.
func cronExecutionFrom(e sqlcdb.CronExecution) CronExecution {
return CronExecution{
ID: e.ID.String(),
CronJobID: e.CronJobID.String(),
StartedAt: e.StartedAt,
FinishedAt: timePtrNT(e.FinishedAt),
Status: e.Status.String,
Output: e.Output.String,
Error: e.Error.String,
}
}
// cronJobProjectID resolves the project a cron job belongs to. // cronJobProjectID resolves the project a cron job belongs to.
func cronJobProjectID(db *database.DB, jobID string) (uuid.UUID, bool) { func cronJobProjectID(db *database.DB, jobID string) (uuid.UUID, bool) {
id, err := uuid.Parse(jobID) var projectID uuid.UUID
if err != nil { err := db.QueryRow(
return uuid.Nil, false `SELECT cj.project_id FROM cron_jobs cj WHERE cj.id = $1`,
} jobID,
projectID, err := sqlcdb.New(db.DB).GetCronJobProjectID(context.Background(), id) ).Scan(&projectID)
if err != nil { if err != nil {
return uuid.Nil, false return uuid.Nil, false
} }
@@ -171,14 +137,15 @@ func handleCreateCronJob(c *gin.Context) {
return return
} }
svcID, err := uuid.Parse(req.ServiceID) var ownerCheck string
if err != nil { err := db.QueryRow(
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) `SELECT p.owner_id FROM projects p
return JOIN services s ON s.project_id = p.id
} WHERE s.id = $1`,
ownerCheck, err := sqlcdb.New(db.DB).GetCronOwnerViaService(context.Background(), svcID) req.ServiceID,
).Scan(&ownerCheck)
if err != nil || (ownerCheck.String() != userID && !contextIsAdmin(c)) { if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -211,20 +178,11 @@ func handleCreateCronJob(c *gin.Context) {
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
err = sqlcdb.New(db.DB).InsertCronJob(context.Background(), sqlcdb.InsertCronJobParams{ _, err = db.Exec(
ID: uuid.MustParse(job.ID), `INSERT INTO cron_jobs (id, project_id, service_id, name, schedule, command, timezone, enabled, next_run_at, retention, created_at, updated_at)
ProjectID: uuid.MustParse(job.ProjectID), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
ServiceID: uuid.MustParse(job.ServiceID), job.ID, job.ProjectID, job.ServiceID, job.Name, job.Schedule, job.Command, job.Timezone, job.Enabled, job.NextRunAt, job.Retention, job.CreatedAt, job.UpdatedAt,
Name: job.Name, )
Schedule: job.Schedule,
Command: job.Command,
Timezone: sql.NullString{String: job.Timezone, Valid: true},
Enabled: sql.NullBool{Bool: job.Enabled, Valid: true},
NextRunAt: ntPtr(job.NextRunAt),
Retention: sql.NullInt32{Int32: int32(job.Retention), Valid: true},
CreatedAt: sql.NullTime{Time: job.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: job.UpdatedAt, Valid: true},
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create cron job"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create cron job"})
@@ -243,17 +201,22 @@ func handleGetCronJob(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
jobID := c.Param("id") jobID := c.Param("id")
jid, err := uuid.Parse(jobID) var job CronJob
err := db.QueryRow(
`SELECT cj.id, cj.project_id, cj.service_id, cj.name, cj.schedule, cj.timezone,
cj.enabled, cj.last_run_at, cj.next_run_at, cj.last_status, cj.last_output,
cj.retention, cj.created_at, cj.updated_at
FROM cron_jobs cj
WHERE cj.id = $1`,
jobID,
).Scan(&job.ID, &job.ProjectID, &job.ServiceID, &job.Name, &job.Schedule, &job.Timezone,
&job.Enabled, &job.LastRunAt, &job.NextRunAt, &job.LastStatus, &job.LastOutput,
&job.Retention, &job.CreatedAt, &job.UpdatedAt)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Cron job not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Cron job not found"})
return return
} }
row, err := sqlcdb.New(db.DB).GetCronJob(context.Background(), jid)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Cron job not found"})
return
}
job := cronJobFrom(row)
projectID, found := cronJobProjectID(db, jobID) projectID, found := cronJobProjectID(db, jobID)
if !found { if !found {
@@ -279,14 +242,15 @@ func handleUpdateCronJob(c *gin.Context) {
return return
} }
jid, err := uuid.Parse(jobID) var ownerCheck string
if err != nil { err := db.QueryRow(
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) `SELECT p.owner_id FROM cron_jobs cj
return JOIN projects p ON cj.project_id = p.id
} WHERE cj.id = $1`,
ownerCheck, err := sqlcdb.New(db.DB).GetCronJobOwner(context.Background(), jid) jobID,
).Scan(&ownerCheck)
if err != nil || (ownerCheck.String() != userID && !contextIsAdmin(c)) { if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -348,19 +312,21 @@ func handleDeleteCronJob(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
jobID := c.Param("id") jobID := c.Param("id")
jid, err := uuid.Parse(jobID) var ownerCheck string
err := db.QueryRow(
`SELECT p.owner_id FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1`,
jobID,
).Scan(&ownerCheck)
if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
_, err = db.Exec("DELETE FROM cron_jobs WHERE id = $1", jobID)
if err != nil { if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
ownerCheck, err := sqlcdb.New(db.DB).GetCronJobOwner(context.Background(), jid)
if err != nil || (ownerCheck.String() != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
if err := sqlcdb.New(db.DB).DeleteCronJob(context.Background(), jid); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cron job"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cron job"})
return return
} }
@@ -384,20 +350,29 @@ func handleGetCronExecutions(c *gin.Context) {
return return
} }
jid, err := uuid.Parse(jobID) rows, err := db.Query(
if err != nil { `SELECT id, cron_job_id, started_at, finished_at, status, output, error
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"}) FROM cron_executions
return WHERE cron_job_id = $1
} ORDER BY started_at DESC
rows, err := sqlcdb.New(db.DB).ListCronExecutions(context.Background(), jid) LIMIT 100`,
jobID,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"})
return return
} }
defer rows.Close()
var executions []CronExecution var executions []CronExecution
for _, e := range rows { for rows.Next() {
executions = append(executions, cronExecutionFrom(e)) var exec CronExecution
err := rows.Scan(&exec.ID, &exec.CronJobID, &exec.StartedAt, &exec.FinishedAt, &exec.Status, &exec.Output, &exec.Error)
if err != nil {
continue
}
executions = append(executions, exec)
} }
c.JSON(http.StatusOK, gin.H{"executions": executions}) c.JSON(http.StatusOK, gin.H{"executions": executions})
@@ -408,33 +383,29 @@ func handleTriggerCronJob(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
jobID := c.Param("id") jobID := c.Param("id")
jid, err := uuid.Parse(jobID) var job CronJob
if err != nil { var ownerCheck string
err := db.QueryRow(
`SELECT cj.service_id, cj.command, cj.schedule, cj.timezone, cj.retention, p.owner_id
FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1`,
jobID,
).Scan(&job.ServiceID, &job.Command, &job.Schedule, &job.Timezone, &job.Retention, &ownerCheck)
if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
trig, err := sqlcdb.New(db.DB).GetCronJobForTrigger(context.Background(), jid)
if err != nil || (trig.OwnerID.String() != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
job := CronJob{
ServiceID: trig.ServiceID.String(),
Command: trig.Command,
Schedule: trig.Schedule,
Timezone: trig.Timezone.String,
Retention: int(trig.Retention.Int32),
}
execID := uuid.New().String() execID := uuid.New().String()
now := time.Now() now := time.Now()
_ = sqlcdb.New(db.DB).InsertCronExecution(context.Background(), sqlcdb.InsertCronExecutionParams{ _, err = db.Exec(
ID: uuid.MustParse(execID), `INSERT INTO cron_executions (id, cron_job_id, started_at, status)
CronJobID: jid, VALUES ($1, $2, $3, $4)`,
StartedAt: now, execID, jobID, now, "running",
Status: sql.NullString{String: "running", Valid: true}, )
})
dockerClient, _ := c.Get("docker_client") dockerClient, _ := c.Get("docker_client")
dockerCli, _ := dockerClient.(*docker.Client) dockerCli, _ := dockerClient.(*docker.Client)
@@ -487,25 +458,39 @@ func StartCronScheduler(ctx context.Context, db *database.DB, dockerClient *dock
} }
func runDueCronJobs(db *database.DB, dockerClient *docker.Client) { func runDueCronJobs(db *database.DB, dockerClient *docker.Client) {
q := sqlcdb.New(db.DB) rows, err := db.Query(
jobs, err := q.ListDueCronJobs(context.Background()) `SELECT id, project_id, service_id, name, schedule, command, timezone, enabled, retention
FROM cron_jobs
WHERE enabled = TRUE AND next_run_at IS NOT NULL AND next_run_at <= NOW()`)
if err != nil { if err != nil {
log.Printf("cron scheduler: failed to list due jobs: %v", err) log.Printf("cron scheduler: failed to list due jobs: %v", err)
return return
} }
defer rows.Close()
type dueJob struct {
CronJob
Command string
}
var jobs []dueJob
for rows.Next() {
var j dueJob
if err := rows.Scan(&j.ID, &j.ProjectID, &j.ServiceID, &j.Name, &j.Schedule,
&j.Command, &j.Timezone, &j.Enabled, &j.Retention); err != nil {
continue
}
jobs = append(jobs, j)
}
for _, j := range jobs { for _, j := range jobs {
execID := uuid.New().String() execID := uuid.New().String()
if err := q.InsertCronExecution(context.Background(), sqlcdb.InsertCronExecutionParams{ if _, err := db.Exec(
ID: uuid.MustParse(execID), `INSERT INTO cron_executions (id, cron_job_id, started_at, status) VALUES ($1, $2, $3, $4)`,
CronJobID: j.ID, execID, j.ID, time.Now(), "running"); err != nil {
StartedAt: time.Now(),
Status: sql.NullString{String: "running", Valid: true},
}); err != nil {
log.Printf("cron scheduler: failed to record execution for %s: %v", j.ID, err) log.Printf("cron scheduler: failed to record execution for %s: %v", j.ID, err)
continue continue
} }
go executeCronJob(db, dockerClient, j.ID.String(), j.ServiceID.String(), execID, j.Command, j.Schedule, j.Timezone.String, int(j.Retention.Int32)) go executeCronJob(db, dockerClient, j.ID, j.ServiceID, execID, j.Command, j.Schedule, j.Timezone, j.Retention)
} }
} }
@@ -599,32 +584,23 @@ func executeCronJob(db *database.DB, dockerClient *docker.Client, jobID, service
} }
now := time.Now() now := time.Now()
q := sqlcdb.New(db.DB) db.Exec(
if eid, err := uuid.Parse(execID); err == nil { `UPDATE cron_executions SET finished_at = $1, status = $2, output = $3, error = $4 WHERE id = $5`,
_ = q.FinishCronExecution(context.Background(), sqlcdb.FinishCronExecutionParams{ now, status, output, errText, execID,
FinishedAt: sql.NullTime{Time: now, Valid: true}, )
Status: sql.NullString{String: status, Valid: true},
Output: sql.NullString{String: output, Valid: true},
Error: sql.NullString{String: errText, Valid: true},
ID: eid,
})
}
next, _ := calculateNextRun(schedule, timezone) next, _ := calculateNextRun(schedule, timezone)
if jid, err := uuid.Parse(jobID); err == nil { db.Exec(
_ = q.UpdateCronJobRun(context.Background(), sqlcdb.UpdateCronJobRunParams{ `UPDATE cron_jobs SET last_run_at = $1, last_status = $2, last_output = $3, next_run_at = $4 WHERE id = $5`,
LastRunAt: sql.NullTime{Time: now, Valid: true}, now, status, output, next, jobID,
LastStatus: sql.NullString{String: status, Valid: true}, )
LastOutput: sql.NullString{String: output, Valid: true},
NextRunAt: ntPtr(next),
ID: jid,
})
if retention > 0 { if retention > 0 {
_ = q.TrimCronExecutions(context.Background(), sqlcdb.TrimCronExecutionsParams{ db.Exec(
CronJobID: jid, `DELETE FROM cron_executions WHERE cron_job_id = $1 AND id NOT IN (
Limit: int32(retention), SELECT id FROM cron_executions WHERE cron_job_id = $1
}) ORDER BY started_at DESC LIMIT $2)`,
} jobID, retention,
)
} }
} }
-127
View File
@@ -1,16 +1,9 @@
package api package api
import ( import (
"context"
"database/sql"
"strings" "strings"
"testing" "testing"
"time" "time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
) )
func TestCalculateNextRun(t *testing.T) { func TestCalculateNextRun(t *testing.T) {
@@ -71,123 +64,3 @@ func TestCalculateNextRunTimezone(t *testing.T) {
t.Logf("utc next: %v / tokyo next: %v", utcNext, tokyoNext) t.Logf("utc next: %v / tokyo next: %v", utcNext, tokyoNext)
} }
} }
// TestCronLiveInvariants exercises the cron.sql queries on live Postgres:
// insert/read/owner join, execution lifecycle, run bookkeeping, retention trim.
func TestCronLiveInvariants(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()
jobID := uuid.New()
suffix := strings.ReplaceAll(jobID.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, "cron-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "cron-"+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)
VALUES ($1, $2, $3, $4, 'web', 'image')`,
serviceID, "cron-"+suffix, projectID, envID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }()
next, err := calculateNextRun("0 4 * * *", "UTC")
if err != nil {
t.Fatalf("schedule: %v", err)
}
must(q.InsertCronJob(ctx, sqlcdb.InsertCronJobParams{
ID: jobID, ProjectID: projectID, ServiceID: serviceID,
Name: "job-" + suffix, Schedule: "0 4 * * *", Command: "echo hi",
Timezone: sql.NullString{String: "UTC", Valid: true},
Enabled: sql.NullBool{Bool: true, Valid: true},
NextRunAt: ntPtr(next),
}))
job, err := q.GetCronJob(ctx, jobID)
if err != nil || job.Name != "job-"+suffix || job.Command != "echo hi" {
t.Fatalf("job: %v %+v", err, job)
}
owner, err := q.GetCronJobOwner(ctx, jobID)
if err != nil || owner != userID {
t.Fatalf("owner: %v %v", err, owner)
}
owner2, err := q.GetCronOwnerViaService(ctx, serviceID)
if err != nil || owner2 != userID {
t.Fatalf("owner via service: %v %v", err, owner2)
}
pid, err := q.GetCronJobProjectID(ctx, jobID)
if err != nil || pid != projectID {
t.Fatalf("project: %v %v", err, pid)
}
// Trigger-shaped read.
trig, err := q.GetCronJobForTrigger(ctx, jobID)
if err != nil || trig.ServiceID != serviceID || trig.OwnerID != userID {
t.Fatalf("trigger: %v %+v", err, trig)
}
// Execution lifecycle + retention trim.
for i := 0; i < 3; i++ {
execID := uuid.New()
must(q.InsertCronExecution(ctx, sqlcdb.InsertCronExecutionParams{
ID: execID, CronJobID: jobID,
StartedAt: time.Now().Add(time.Duration(i) * time.Second),
Status: sql.NullString{String: "running", Valid: true},
}))
must(q.FinishCronExecution(ctx, sqlcdb.FinishCronExecutionParams{
FinishedAt: sql.NullTime{Time: time.Now(), Valid: true},
Status: sql.NullString{String: "success", Valid: true},
Output: sql.NullString{String: "ok", Valid: true},
Error: sql.NullString{String: "", Valid: true},
ID: execID,
}))
}
execs, err := q.ListCronExecutions(ctx, jobID)
if err != nil || len(execs) != 3 {
t.Fatalf("execs: %v %d", err, len(execs))
}
if execs[0].Status.String != "success" || !execs[0].FinishedAt.Valid {
t.Fatalf("exec row: %+v", execs[0])
}
must(q.TrimCronExecutions(ctx, sqlcdb.TrimCronExecutionsParams{CronJobID: jobID, Limit: 1}))
execs, _ = q.ListCronExecutions(ctx, jobID)
if len(execs) != 1 {
t.Fatalf("trim kept %d, want 1", len(execs))
}
must(q.UpdateCronJobRun(ctx, sqlcdb.UpdateCronJobRunParams{
LastRunAt: sql.NullTime{Time: time.Now(), Valid: true},
LastStatus: sql.NullString{String: "success", Valid: true},
LastOutput: sql.NullString{String: "ok", Valid: true},
NextRunAt: ntPtr(next),
ID: jobID,
}))
job, _ = q.GetCronJob(ctx, jobID)
if job.LastStatus.String != "success" || !job.LastRunAt.Valid {
t.Fatalf("post-run job: %+v", job)
}
must(q.DeleteCronJob(ctx, jobID))
if _, err := q.GetCronJob(ctx, jobID); err == nil {
t.Fatal("deleted job still readable")
}
}
+278 -270
View File
@@ -2,14 +2,12 @@ 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"
@@ -39,72 +37,6 @@ 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"`
@@ -152,29 +84,44 @@ func handleGetDeployments(c *gin.Context) {
offset, limit := pageWindow(c, 50, 100) offset, limit := pageWindow(c, 50, 100)
q := sqlcdb.New(db.(*database.DB).DB) var total int
total, _ := q.CountDeploymentsForService(c.Request.Context(), serviceID) _ = db.(*database.DB).QueryRow(
`SELECT COUNT(*) FROM deployments WHERE service_id = $1`, serviceID,
).Scan(&total)
rows, err := q.ListDeploymentsForService(c.Request.Context(), sqlcdb.ListDeploymentsForServiceParams{ rows, err := db.(*database.DB).Query(
ServiceID: serviceID, Limit: int32(limit), Offset: int32(offset)}) `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`,
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()
deployments := make([]DeploymentModel, 0, len(rows)) var deployments []DeploymentModel
for _, r := range rows { for rows.Next() {
deployments = append(deployments, deploymentModelFrom(deploymentRowShape{ var d DeploymentModel
ID: r.ID, ServiceID: r.ServiceID, CommitHash: r.CommitHash, Status: r.Status, err := rows.Scan(
ImageName: r.ImageName, ImageTag: r.ImageTag, BuildLog: r.BuildLog, &d.ID, &d.ServiceID, &d.CommitHash, &d.Status, &d.ImageName, &d.ImageTag,
RuntimeLog: r.RuntimeLog, Error: r.Error, StartedAt: r.StartedAt, &d.BuildLog, &d.RuntimeLog, &d.Error, &d.StartedAt, &d.CompletedAt,
CompletedAt: r.CompletedAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, &d.CreatedAt, &d.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, int(total)), "pagination": paginationMeta(offset, limit, total),
}) })
} }
@@ -205,33 +152,51 @@ func handleGetRecentDeployments(c *gin.Context) {
offset, limit := pageWindow(c, 10, 50) offset, limit := pageWindow(c, 10, 50)
q := sqlcdb.New(db.(*database.DB).DB) var total int
total, err := q.CountAccessibleDeployments(c.Request.Context(), sqlcdb.CountAccessibleDeploymentsParams{ if err := db.(*database.DB).QueryRow(
OwnerID: userID, Column2: isAdmin}) `SELECT COUNT(*)
if err != nil { 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)`,
userID, isAdmin,
).Scan(&total); err != nil {
total = 0 total = 0
} }
rows, err := q.ListRecentAccessibleDeployments(c.Request.Context(), sqlcdb.ListRecentAccessibleDeploymentsParams{ rows, err := db.(*database.DB).Query(
OwnerID: userID, Column2: isAdmin, Limit: int32(limit), Offset: int32(offset)}) `SELECT d.id, d.service_id, s.name, p.name, d.status,
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 := make([]RecentDeployment, 0, len(rows)) deployments := []RecentDeployment{}
for _, r := range rows { for rows.Next() {
deployments = append(deployments, RecentDeployment{ var d RecentDeployment
ID: r.ID, ServiceID: r.ServiceID, ServiceName: r.ServiceName, if err := rows.Scan(&d.ID, &d.ServiceID, &d.ServiceName, &d.ProjectName,
ProjectName: r.ProjectName, Status: r.Status.String, ImageName: r.ImageName, &d.Status, &d.ImageName, &d.StartedAt, &d.CompletedAt, &d.CreatedAt); err != nil {
StartedAt: timePtrNT(r.StartedAt), CompletedAt: timePtrNT(r.CompletedAt), c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan deployment"})
CreatedAt: r.CreatedAt.Time, 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, int(total)), "pagination": paginationMeta(offset, limit, total),
}) })
} }
@@ -265,27 +230,38 @@ func handleCreateDeployment(c *gin.Context) {
return return
} }
ctx := c.Request.Context() var service Service
q := sqlcdb.New(db.(*database.DB).DB) var projectOwner string
sr, err := q.GetServiceForDeployWithOwner(ctx, serviceID) err = 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, 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.String() != userID.(string) { if projectOwner != userID.(string) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -311,17 +287,12 @@ func handleCreateDeployment(c *gin.Context) {
UpdatedAt: now, UpdatedAt: now,
} }
err = q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{ _, err = db.(*database.DB).Exec(
ID: d.ID, `INSERT INTO deployments
ServiceID: d.ServiceID, (id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
Version: fmt.Sprintf("v%d", now.Unix()), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
CommitHash: sql.NullString{String: ptrStr(d.CommitHash), Valid: d.CommitHash != nil}, d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt,
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"})
@@ -332,20 +303,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()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.(*database.DB).Exec(
Error: sql.NullString{String: unavailableErr, Valid: true}, `UPDATE deployments
CompletedAt: sql.NullTime{Time: completedAt, Valid: true}, SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
ID: d.ID, WHERE id = $3`,
}) 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 = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, err = db.(*database.DB).Exec(
Status: sql.NullString{String: "building", Valid: true}, `UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, time.Now(), serviceID,
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
@@ -360,17 +331,14 @@ func handleCreateDeployment(c *gin.Context) {
}) })
if pos > 0 { if pos > 0 {
d.Status = "queued" d.Status = "queued"
now := time.Now() _, _ = db.(*database.DB).Exec(
_ = q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{ `UPDATE deployments SET status = 'queued', updated_at = $1 WHERE id = $2`,
Status: sql.NullString{String: "queued", Valid: true}, time.Now(), d.ID,
UpdatedAt: sql.NullTime{Time: now, Valid: true}, )
ID: d.ID, _, _ = db.(*database.DB).Exec(
}) `UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`,
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ time.Now(), serviceID,
Status: sql.NullString{String: "queued", Valid: true}, )
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: serviceID,
})
} }
} }
@@ -412,7 +380,6 @@ 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
@@ -425,11 +392,10 @@ func runDeploymentAndSyncWithImage(
}() }()
failDeploy := func(msg string) { failDeploy := func(msg string) {
failedAt := time.Now() failedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.Exec(
Error: sql.NullString{String: msg, Valid: true}, `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, msg, failedAt, dbDeployment.ID,
ID: dbDeployment.ID, )
})
} }
sourcePath := strings.TrimSpace(service.BuildPath) sourcePath := strings.TrimSpace(service.BuildPath)
@@ -457,10 +423,11 @@ 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.
publishedPort, _ := q.GetServicePublishedPort(ctx, service.ID) _ = db.QueryRow(`SELECT published_port FROM services WHERE id = $1`, service.ID).Scan(&publishedPort)
publicPort := int32(service.Port) publicPort := int32(service.Port)
if service.Builder == "static" && publicPort == 0 { if service.Builder == "static" && publicPort == 0 {
@@ -484,9 +451,7 @@ 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
if o, err := q.GetProjectOwner(ctx, service.ProjectID); err == nil { _ = db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, service.ProjectID).Scan(&ownerID)
ownerID = o.String()
}
branch := req.Branch branch := req.Branch
if branch == "" { if branch == "" {
branch = service.GitBranch branch = service.GitBranch
@@ -570,9 +535,7 @@ func runDeploymentAndSyncWithImage(
} }
} else { } else {
var ownerID string var ownerID string
if o, err := q.GetProjectOwner(ctx, service.ProjectID); err == nil { _ = db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, service.ProjectID).Scan(&ownerID)
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",
@@ -642,11 +605,10 @@ 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()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.Exec(
Error: sql.NullString{String: msg, Valid: true}, `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, msg, failedAt, dbDeployment.ID,
ID: dbDeployment.ID, )
})
return return
} }
@@ -654,16 +616,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()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.Exec(
Error: sql.NullString{String: failure, Valid: true}, `UPDATE deployments
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
ID: dbDeployment.ID, WHERE id = $3`,
}) failure, failedAt, dbDeployment.ID,
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ )
Status: sql.NullString{String: "failed", Valid: true}, _, _ = db.Exec(
UpdatedAt: sql.NullTime{Time: failedAt, Valid: true}, `UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
ID: service.ID, failedAt, service.ID,
}) )
return return
} }
@@ -678,17 +640,16 @@ 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"
} }
_ = q.CompleteDeployment(ctx, sqlcdb.CompleteDeploymentParams{ _, _ = db.Exec(
Status: sql.NullString{String: finalStatus, Valid: true}, `UPDATE deployments
Error: sql.NullString{String: finalErr, Valid: true}, SET status = $1, error = $2, completed_at = $3, updated_at = $3
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, WHERE id = $4`,
ID: dbDeployment.ID, finalStatus, finalErr, failedAt, dbDeployment.ID,
}) )
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, _ = db.Exec(
Status: sql.NullString{String: finalStatus, Valid: true}, `UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`,
UpdatedAt: sql.NullTime{Time: failedAt, Valid: true}, finalStatus, failedAt, service.ID,
ID: service.ID, )
})
return return
case <-syncTicker.C: case <-syncTicker.C:
current, getErr := engine.GetDeployment(engineDeployment.ID) current, getErr := engine.GetDeployment(engineDeployment.ID)
@@ -699,36 +660,49 @@ func runDeploymentAndSyncWithImage(
dbStatus := mapEngineStatusToDBStatus(current.Status) dbStatus := mapEngineStatusToDBStatus(current.Status)
imageName, imageTag := splitImageReference(current.ImageName, dbDeployment.ImageTag) imageName, imageTag := splitImageReference(current.ImageName, dbDeployment.ImageTag)
now := time.Now() var dbError interface{}
_ = q.SyncDeploymentProgress(ctx, sqlcdb.SyncDeploymentProgressParams{ if current.Error != "" {
Status: sql.NullString{String: dbStatus, Valid: true}, dbError = current.Error
ImageName: sql.NullString{String: imageName, Valid: true}, }
ImageTag: sql.NullString{String: imageTag, Valid: true},
BuildLog: sql.NullString{String: current.BuildLog, Valid: true}, _, _ = db.Exec(
RuntimeLog: sql.NullString{String: current.DeployLog, Valid: true}, `UPDATE deployments
Error: sql.NullString{String: current.Error, Valid: current.Error != ""}, SET status = $1,
StartedAt: ntPtr(current.StartedAt), image_name = $2,
CompletedAt: ntPtr(current.CompletedAt), image_tag = $3,
UpdatedAt: sql.NullTime{Time: now, Valid: true}, build_log = $4,
ID: dbDeployment.ID, runtime_log = $5,
}) 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":
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, _ = db.Exec(
Status: sql.NullString{String: "running", Valid: true}, `UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: now, Valid: true}, time.Now(), service.ID,
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":
_ = 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: now, Valid: true}, time.Now(), service.ID,
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)
@@ -780,16 +754,21 @@ func handleCancelDeployment(c *gin.Context) {
return return
} }
ctx := c.Request.Context() var status, owner string
q := sqlcdb.New(db.(*database.DB).DB) var serviceID uuid.UUID
access, err := q.GetDeploymentAccess(ctx, deploymentID) err = db.(*database.DB).QueryRow(
`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
} }
serviceID := access.ServiceID if owner != userID.(string) && !contextIsAdmin(c) {
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
} }
@@ -803,17 +782,14 @@ 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()
_ = q.CompleteDeployment(ctx, sqlcdb.CompleteDeploymentParams{ _, _ = db.(*database.DB).Exec(
Status: sql.NullString{String: "cancelled", Valid: true}, `UPDATE deployments SET status = 'cancelled', error = 'Deployment cancelled', completed_at = $1, updated_at = $1 WHERE id = $2`,
Error: sql.NullString{String: "Deployment cancelled", Valid: true}, now, deploymentID,
CompletedAt: sql.NullTime{Time: now, Valid: true}, )
ID: deploymentID, _, _ = db.(*database.DB).Exec(
}) `UPDATE services SET status = 'cancelled', updated_at = $1 WHERE id = $2`,
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ now, serviceID,
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"})
@@ -856,18 +832,27 @@ func handleGetDeployment(c *gin.Context) {
return return
} }
r, err := sqlcdb.New(db.(*database.DB).DB).GetDeploymentWithProject(c.Request.Context(), deploymentID) var d DeploymentModel
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"})
@@ -897,23 +882,33 @@ func handleRollbackDeployment(c *gin.Context) {
return return
} }
ctx := c.Request.Context() var targetDeployment DeploymentModel
q := sqlcdb.New(db.(*database.DB).DB) var serviceID uuid.UUID
r, err := q.GetDeploymentForRollback(ctx, deploymentID) var ownerCheck string
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.String() != userID.(string) && !contextIsAdmin(c) { if ownerCheck != userID.(string) && !contextIsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -936,28 +931,23 @@ func handleRollbackDeployment(c *gin.Context) {
UpdatedAt: now, UpdatedAt: now,
} }
err = q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{ _, err = db.(*database.DB).Exec(
ID: rollback.ID, `INSERT INTO deployments
ServiceID: rollback.ServiceID, (id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
Version: fmt.Sprintf("rollback-%d", now.Unix()), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
CommitHash: sql.NullString{String: ptrStr(rollback.CommitHash), Valid: rollback.CommitHash != nil}, rollback.ID, rollback.ServiceID, fmt.Sprintf("rollback-%d", now.Unix()), rollback.CommitHash, rollback.Status,
Status: sql.NullString{String: rollback.Status, Valid: true}, rollback.ImageName, rollback.ImageTag, rollback.CreatedAt, rollback.UpdatedAt,
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 = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, err = db.(*database.DB).Exec(
Status: sql.NullString{String: "building", Valid: true}, `UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, time.Now(), serviceID,
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")
@@ -973,18 +963,36 @@ 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."
} }
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.(*database.DB).Exec(
Error: sql.NullString{String: reason, Valid: true}, `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
CompletedAt: sql.NullTime{Time: completedAt, Valid: true}, reason, completedAt, rollbackID,
ID: rollbackID, )
}) _, _ = db.(*database.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}, completedAt, serviceID,
UpdatedAt: sql.NullTime{Time: completedAt, Valid: true}, )
ID: serviceID,
})
} else { } else {
service, _ := loadServiceForDeploy(db.(*database.DB), serviceID) var service Service
_ = 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 {
+51 -65
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"net/http" "net/http"
"regexp" "regexp"
@@ -45,26 +44,19 @@ func validHostname(d string) bool {
} }
func loadServiceDomains(db *database.DB, serviceID uuid.UUID) []ServiceDomain { func loadServiceDomains(db *database.DB, serviceID uuid.UUID) []ServiceDomain {
rows, err := sqlcdb.New(db.DB).ListServiceDomains(context.Background(), serviceID) rows, err := db.Query(
`SELECT id, service_id, domain, is_default, cert_type, cert_status, last_checked_at, created_at
FROM service_domains WHERE service_id = $1 ORDER BY is_default DESC, created_at ASC`, serviceID)
if err != nil { if err != nil {
return nil return nil
} }
out := make([]ServiceDomain, 0, len(rows)) defer rows.Close()
for _, r := range rows { out := []ServiceDomain{}
d := ServiceDomain{ for rows.Next() {
ID: r.ID, var d ServiceDomain
ServiceID: r.ServiceID, if err := rows.Scan(&d.ID, &d.ServiceID, &d.Domain, &d.IsDefault, &d.CertType, &d.CertStatus, &d.LastCheckedAt, &d.CreatedAt); err == nil {
Domain: r.Domain, out = append(out, d)
IsDefault: r.IsDefault,
CertType: r.CertType,
CertStatus: r.CertStatus,
CreatedAt: r.CreatedAt,
} }
if r.LastCheckedAt.Valid {
t := r.LastCheckedAt.Time
d.LastCheckedAt = &t
}
out = append(out, d)
} }
return out return out
} }
@@ -112,20 +104,22 @@ func dnsLabel(name string) string {
// base domain; spread services union every eligible node's base domain. // base domain; spread services union every eligible node's base domain.
// Local services get nothing — there is no instance-level base domain yet. // Local services get nothing — there is no instance-level base domain yet.
func serviceAutoDomains(db *database.DB, serviceID uuid.UUID) []string { func serviceAutoDomains(db *database.DB, serviceID uuid.UUID) []string {
ctx := context.Background() var name string
q := sqlcdb.New(db.DB) var nodeID sql.NullString
brief, err := q.GetServicePlacementBrief(ctx, serviceID) var spread bool
if err != nil { if err := db.QueryRow(
`SELECT name, node_id, COALESCE(spread, false) FROM services WHERE id = $1`, serviceID,
).Scan(&name, &nodeID, &spread); err != nil {
return nil return nil
} }
label := dnsLabel(brief.Name) label := dnsLabel(name)
if label == "" { if label == "" {
return nil return nil
} }
var bases []string var bases []string
nodeID := brief.NodeID ctx := context.Background()
spread := brief.Spread q := sqlcdb.New(db.DB)
switch { switch {
case nodeID.Valid && nodeID.String != "": case nodeID.Valid && nodeID.String != "":
if a, err := q.GetAgent(ctx, nodeID.String); err == nil && a.DefaultDomain != "" { if a, err := q.GetAgent(ctx, nodeID.String); err == nil && a.DefaultDomain != "" {
@@ -148,25 +142,24 @@ func serviceAutoDomains(db *database.DB, serviceID uuid.UUID) []string {
// syncDefaultDomain writes the is_default row back to services.domain so // syncDefaultDomain writes the is_default row back to services.domain so
// legacy reads (CLI tables, public_url) keep working. // legacy reads (CLI tables, public_url) keep working.
func syncDefaultDomain(db *database.DB, serviceID uuid.UUID) { func syncDefaultDomain(db *database.DB, serviceID uuid.UUID) {
ctx := context.Background() var domain string
q := sqlcdb.New(db.DB) err := db.QueryRow(
domain, err := q.GetServiceDomainDefault(ctx, serviceID) `SELECT domain FROM service_domains WHERE service_id = $1 AND is_default`, serviceID).Scan(&domain)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
domain, err = q.GetServiceDomainFirst(ctx, serviceID) err = db.QueryRow(
`SELECT domain FROM service_domains WHERE service_id = $1 ORDER BY created_at ASC LIMIT 1`, serviceID).Scan(&domain)
} }
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
domain = "" domain = ""
} }
_ = q.SetServiceLegacyDomain(ctx, sqlcdb.SetServiceLegacyDomainParams{Domain: domain, ID: serviceID}) _, _ = db.Exec(`UPDATE services SET domain = $1 WHERE id = $2`, domain, serviceID)
} }
// serviceAccess loads the maintenance/basic-auth columns for the runtime spec. // serviceAccess loads the maintenance/basic-auth columns for the runtime spec.
func serviceAccess(db *database.DB, serviceID uuid.UUID) (maintenance bool, basicAuth string) { func serviceAccess(db *database.DB, serviceID uuid.UUID) (maintenance bool, basicAuth string) {
row, err := sqlcdb.New(db.DB).GetServiceAccess(context.Background(), serviceID) _ = db.QueryRow(`SELECT maintenance_mode, basic_auth_users FROM services WHERE id = $1`, serviceID).
if err != nil { Scan(&maintenance, &basicAuth)
return false, "" return maintenance, basicAuth
}
return row.MaintenanceMode, row.BasicAuthUsers
} }
// maintenanceURL is the redirect target for maintenance mode — the public // maintenanceURL is the redirect target for maintenance mode — the public
@@ -211,12 +204,11 @@ 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, _ := uuid.Parse(fmt.Sprint(userID)) var ok bool
ok, err := sqlcdb.New(db.DB).ServiceWriteAccess(context.Background(), sqlcdb.ServiceWriteAccessParams{ err := db.QueryRow(
ID: serviceID, `SELECT EXISTS(SELECT 1 FROM services s JOIN projects p ON s.project_id = p.id
OwnerID: uid, WHERE s.id = $1 AND (p.owner_id = $2 OR $3::bool))`,
Column3: contextIsAdmin(c), serviceID, userID, contextIsAdmin(c)).Scan(&ok)
})
return err == nil && ok return err == nil && ok
} }
@@ -263,11 +255,9 @@ func handleAddServiceDomain(c *gin.Context) {
return return
} }
ctx := c.Request.Context()
q := sqlcdb.New(db.(*database.DB).DB)
// First domain on a service becomes the default automatically. // First domain on a service becomes the default automatically.
count, _ := q.CountServiceDomains(ctx, serviceID) var count int
_ = db.(*database.DB).QueryRow(`SELECT COUNT(*) FROM service_domains WHERE service_id = $1`, serviceID).Scan(&count)
makeDefault := req.IsDefault || count == 0 makeDefault := req.IsDefault || count == 0
tx, err := db.(*database.DB).Begin() tx, err := db.(*database.DB).Begin()
@@ -276,18 +266,19 @@ func handleAddServiceDomain(c *gin.Context) {
return return
} }
defer tx.Rollback() defer tx.Rollback()
qtx := q.WithTx(tx)
if makeDefault { if makeDefault {
if err := qtx.ClearServiceDomainDefault(ctx, serviceID); err != nil { if _, err := tx.Exec(`UPDATE service_domains SET is_default = false WHERE service_id = $1`, serviceID); err != nil {
respondError(c, http.StatusInternalServerError, "INTERNAL", "Failed to add domain") respondError(c, http.StatusInternalServerError, "INTERNAL", "Failed to add domain")
return return
} }
} }
row, err := qtx.CreateServiceDomain(ctx, sqlcdb.CreateServiceDomainParams{ var inserted ServiceDomain
ServiceID: serviceID, err = tx.QueryRow(
Domain: domain, `INSERT INTO service_domains (service_id, domain, is_default)
IsDefault: makeDefault, VALUES ($1, $2, $3)
}) RETURNING id, service_id, domain, is_default, cert_type, cert_status, created_at`,
serviceID, domain, makeDefault).
Scan(&inserted.ID, &inserted.ServiceID, &inserted.Domain, &inserted.IsDefault, &inserted.CertType, &inserted.CertStatus, &inserted.CreatedAt)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") { if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
respondError(c, http.StatusConflict, "CONFLICT", "Domain already attached to this service") respondError(c, http.StatusConflict, "CONFLICT", "Domain already attached to this service")
@@ -301,15 +292,7 @@ func handleAddServiceDomain(c *gin.Context) {
return return
} }
syncDefaultDomain(db.(*database.DB), serviceID) syncDefaultDomain(db.(*database.DB), serviceID)
c.JSON(http.StatusCreated, gin.H{"domain": ServiceDomain{ c.JSON(http.StatusCreated, gin.H{"domain": inserted})
ID: row.ID,
ServiceID: row.ServiceID,
Domain: row.Domain,
IsDefault: row.IsDefault,
CertType: row.CertType,
CertStatus: row.CertStatus,
CreatedAt: row.CreatedAt,
}})
} }
func handleDeleteServiceDomain(c *gin.Context) { func handleDeleteServiceDomain(c *gin.Context) {
@@ -332,8 +315,9 @@ func handleDeleteServiceDomain(c *gin.Context) {
respondError(c, http.StatusBadRequest, "VALIDATION", "Invalid domain ID") respondError(c, http.StatusBadRequest, "VALIDATION", "Invalid domain ID")
return return
} }
affected, err := sqlcdb.New(db.(*database.DB).DB).DeleteServiceDomain( res, err := db.(*database.DB).Exec(
c.Request.Context(), sqlcdb.DeleteServiceDomainParams{ID: domainID, ServiceID: serviceID}) `DELETE FROM service_domains WHERE id = $1 AND service_id = $2`, domainID, serviceID)
affected, _ := res.RowsAffected()
if err != nil || affected == 0 { if err != nil || affected == 0 {
respondError(c, http.StatusNotFound, "NOT_FOUND", "Domain not found") respondError(c, http.StatusNotFound, "NOT_FOUND", "Domain not found")
return return
@@ -362,12 +346,14 @@ func handleSetDefaultDomain(c *gin.Context) {
respondError(c, http.StatusBadRequest, "VALIDATION", "Invalid domain ID") respondError(c, http.StatusBadRequest, "VALIDATION", "Invalid domain ID")
return return
} }
affected, err := sqlcdb.New(db.(*database.DB).DB).SetServiceDomainDefault( res, err := db.(*database.DB).Exec(
c.Request.Context(), sqlcdb.SetServiceDomainDefaultParams{ID: domainID, ServiceID: serviceID}) `UPDATE service_domains SET is_default = (id = $1) WHERE service_id = $2`,
domainID, serviceID)
if err != nil { if err != nil {
respondError(c, http.StatusInternalServerError, "INTERNAL", "Failed to set default domain") respondError(c, http.StatusInternalServerError, "INTERNAL", "Failed to set default domain")
return return
} }
affected, _ := res.RowsAffected()
if affected == 0 { if affected == 0 {
respondError(c, http.StatusNotFound, "NOT_FOUND", "Domain not found") respondError(c, http.StatusNotFound, "NOT_FOUND", "Domain not found")
return return
@@ -435,7 +421,7 @@ func handleCheckServiceDomains(c *gin.Context) {
r.Status = "ok" r.Status = "ok"
} }
results = append(results, r) results = append(results, r)
_ = sqlcdb.New(dbh.DB).TouchServiceDomainChecked(c.Request.Context(), d.ID) _, _ = dbh.Exec(`UPDATE service_domains SET last_checked_at = NOW() WHERE id = $1`, d.ID)
} }
c.JSON(http.StatusOK, gin.H{"domains": results}) c.JSON(http.StatusOK, gin.H{"domains": results})
} }
+1 -576
View File
@@ -1,30 +1,6 @@
package api package api
import ( import "testing"
"context"
"database/sql"
"os"
"strings"
"testing"
"time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
)
func testDSNString(t *testing.T) string {
t.Helper()
dsn := os.Getenv("METRICS_TEST_DSN")
if dsn == "" {
dsn = os.Getenv("DATABASE_URL")
}
if dsn == "" {
t.Skip("METRICS_TEST_DSN not set")
}
return dsn
}
func TestDNSLabel(t *testing.T) { func TestDNSLabel(t *testing.T) {
cases := map[string]string{ cases := map[string]string{
@@ -48,554 +24,3 @@ func TestDNSLabelLengthCap(t *testing.T) {
t.Fatalf("label over 63 chars: %d", len(got)) t.Fatalf("label over 63 chars: %d", len(got))
} }
} }
// Live invariant test against real Postgres — set METRICS_TEST_DSN (or
// DATABASE_URL). Verifies the generated domain queries preserve the
// default-domain and legacy services.domain invariants.
func TestServiceDomainsLiveInvariants(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, "domtest-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "domtest-"+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)
VALUES ($1, 'domtest-`+suffix+`', $2, $3, 'web', 'image')`,
serviceID, projectID, envID)
must(err)
defer func() {
_, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID)
}()
d1, err := q.CreateServiceDomain(ctx, sqlcdb.CreateServiceDomainParams{
ServiceID: serviceID, Domain: "a-" + suffix + ".example.com", IsDefault: true})
if err != nil {
t.Fatalf("create d1: %v", err)
}
d2, err := q.CreateServiceDomain(ctx, sqlcdb.CreateServiceDomainParams{
ServiceID: serviceID, Domain: "b-" + suffix + ".example.com", IsDefault: false})
if err != nil {
t.Fatalf("create d2: %v", err)
}
syncDefaultDomain(db, serviceID)
var legacy string
if err := db.QueryRow(`SELECT domain FROM services WHERE id = $1`, serviceID).Scan(&legacy); err != nil {
t.Fatalf("read legacy: %v", err)
}
if legacy != d1.Domain {
t.Fatalf("legacy domain = %q, want %q", legacy, d1.Domain)
}
// Switch default: exactly one row stays default, legacy follows.
if err := q.ClearServiceDomainDefault(ctx, serviceID); err != nil {
t.Fatalf("clear: %v", err)
}
if _, err := q.SetServiceDomainDefault(ctx, sqlcdb.SetServiceDomainDefaultParams{
ID: d2.ID, ServiceID: serviceID}); err != nil {
t.Fatalf("set default: %v", err)
}
syncDefaultDomain(db, serviceID)
if err := db.QueryRow(`SELECT domain FROM services WHERE id = $1`, serviceID).Scan(&legacy); err != nil {
t.Fatalf("read legacy2: %v", err)
}
if legacy != d2.Domain {
t.Fatalf("legacy after switch = %q, want %q", legacy, d2.Domain)
}
doms := loadServiceDomains(db, serviceID)
if len(doms) != 2 || !doms[0].IsDefault || doms[0].Domain != d2.Domain {
t.Fatalf("list order wrong: %+v", doms)
}
// Deleting the default falls back to the remaining domain.
if _, err := q.DeleteServiceDomain(ctx, sqlcdb.DeleteServiceDomainParams{
ID: d2.ID, ServiceID: serviceID}); err != nil {
t.Fatalf("delete d2: %v", err)
}
syncDefaultDomain(db, serviceID)
if err := db.QueryRow(`SELECT domain FROM services WHERE id = $1`, serviceID).Scan(&legacy); err != nil {
t.Fatalf("read legacy3: %v", err)
}
if 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)
}
}
// TestRuntimeLiveInvariants exercises the runtime.sql queries: the
// legacy-column COALESCE chain in GetServiceRuntimeWithOwner, the
// deployed-image concat, and the conditional published-port update.
func TestRuntimeLiveInvariants(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, "rt-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "rt-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
// Only legacy columns populated — the COALESCE chain must surface them.
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type,
source_url, image_name, start_command, port) VALUES ($1, $2, $3, $4, 'worker', 'git', $5, $6, 'run.sh', 8080)`,
serviceID, "rt-"+suffix, projectID, envID, "https://git.example/x.git", "repo/img:latest")
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }()
row, err := q.GetServiceRuntimeWithOwner(ctx, serviceID)
if err != nil {
t.Fatalf("runtime row: %v", err)
}
if row.Type != "worker" || row.Image != "repo/img:latest" || row.Command != "run.sh" ||
row.GitRepo != "https://git.example/x.git" || row.OwnerID != userID {
t.Fatalf("COALESCE chain lost legacy columns: %+v", row)
}
svc := serviceFromRuntimeRow(row)
if svc.Type != "worker" || svc.Image != "repo/img:latest" || svc.Port != 8080 {
t.Fatalf("mapped service wrong: %+v", svc)
}
// Sibling refs + port lookup.
refs, err := q.ListProjectServiceRefs(ctx, projectID)
if err != nil || len(refs) != 1 || refs[0].Name != "rt-"+suffix {
t.Fatalf("refs: %v %+v", err, refs)
}
port, err := q.GetServicePort(ctx, serviceID)
if err != nil || port != 8080 {
t.Fatalf("port: %v %d", err, port)
}
// Variables round-trip.
_, err = db.Exec(`INSERT INTO environment_variables (id, service_id, key, value, is_secret)
VALUES (gen_random_uuid(), $1, 'API_KEY', 'shh', true), (gen_random_uuid(), $1, 'PLAIN', 'p', false)`,
serviceID)
must(err)
vars, err := q.ListServiceVariablesWithSecret(ctx, serviceID)
if err != nil || len(vars) != 2 {
t.Fatalf("vars: %v %+v", err, vars)
}
vals, err := q.ListServiceVariableValues(ctx, serviceID)
if err != nil || len(vals) != 2 {
t.Fatalf("vals: %v %+v", err, vals)
}
got, err := q.GetServiceVariableValue(ctx, sqlcdb.GetServiceVariableValueParams{ServiceID: serviceID, Key: "API_KEY"})
if err != nil || got != "shh" {
t.Fatalf("var value: %v %q", err, got)
}
// Node agent listing through container_instances.
_, err = db.Exec(`INSERT INTO node_agents (id, name, hostname, ip_address, port) VALUES ($1, $2, 'h', '127.0.0.1', 9999)`,
"agent-"+suffix, "agent-"+suffix)
must(err)
_, err = db.Exec(`INSERT INTO container_instances (id, name, image, project_id, service_id, node_agent_id)
VALUES ($1, 'c', 'img', $2, $3, $4)`,
"ci-"+suffix, projectID.String(), serviceID.String(), "agent-"+suffix)
must(err)
agents, err := q.ListNodeAgentsForService(ctx, serviceID.String())
if err != nil || len(agents) != 1 || agents[0] != "agent-"+suffix {
t.Fatalf("agents: %v %+v", err, agents)
}
// Last deployed image: only 'deployed' rows, latest first.
d1, d2 := uuid.New(), uuid.New()
must(q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{
ID: d1, ServiceID: serviceID, Version: "v1",
Status: sql.NullString{String: "failed", Valid: true},
}))
must(q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{
ID: d2, ServiceID: serviceID, Version: "v2",
Status: sql.NullString{String: "deployed", Valid: true},
}))
must(q.SyncDeploymentProgress(ctx, sqlcdb.SyncDeploymentProgressParams{
Status: sql.NullString{String: "deployed", Valid: true},
ImageName: sql.NullString{String: "app", Valid: true},
ImageTag: sql.NullString{String: "v2", Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: d2,
}))
img, err := q.GetLastDeployedImage(ctx, serviceID)
if err != nil || img != "app:v2" {
t.Fatalf("last image: %v %q", err, img)
}
// Conditional published-port update: writes once, no-ops on repeat.
must(q.SetServicePublishedPort(ctx, sqlcdb.SetServicePublishedPortParams{PublishedPort: 30001, ID: serviceID}))
p, err := q.GetServicePublishedPort(ctx, serviceID)
if err != nil || p != 30001 {
t.Fatalf("published port: %v %d", err, p)
}
must(q.SetServiceReplicas(ctx, sqlcdb.SetServiceReplicasParams{
Replicas: 3, UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, ID: serviceID}))
row, _ = q.GetServiceRuntimeWithOwner(ctx, serviceID)
if row.Replicas != 3 {
t.Fatalf("replicas: %+v", row)
}
// Sleeper reads: candidate sweep + domain/status text lookups.
_, err = db.Exec(`UPDATE services SET sleep_enabled = true, domain = 'rt.example.com', status = 'running' WHERE id = $1`, serviceID)
must(err)
cands, err := q.ListSleepCandidates(ctx)
if err != nil {
t.Fatalf("sleep candidates: %v", err)
}
var mine *sqlcdb.ListSleepCandidatesRow
for i := range cands {
if cands[i].ID == serviceID {
mine = &cands[i]
}
}
if mine == nil || mine.SleepIdleMinutes != 15 {
t.Fatalf("candidate missing/idle wrong: %+v", mine)
}
dom, err := q.GetServiceDomainText(ctx, serviceID)
if err != nil || dom != "rt.example.com" {
t.Fatalf("domain text: %v %q", err, dom)
}
st, err := q.GetServiceStatusText(ctx, serviceID)
if err != nil || st != "running" {
t.Fatalf("status text: %v %q", err, st)
}
}
+221 -197
View File
@@ -2,8 +2,6 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"context"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
@@ -94,73 +92,41 @@ type CreateWebhookRequest struct {
Branch string `json:"branch"` Branch string `json:"branch"`
} }
func gitRepoFrom(r sqlcdb.ListGitRepositoriesRow) GitRepository {
return GitRepository{
ID: r.ID.String(),
ProviderID: r.ProviderID.String(),
Name: r.Name,
FullName: r.FullName,
Description: r.Description,
CloneURL: r.CloneUrl,
WebhookURL: r.WebhookUrl,
DefaultBranch: r.DefaultBranch.String,
IsPrivate: r.IsPrivate.Bool,
UserID: r.UserID.String(),
CreatedAt: r.CreatedAt.Time,
UpdatedAt: r.UpdatedAt.Time,
}
}
func gitRepoFromFullNameRow(r sqlcdb.GetGitRepositoryByFullNameRow) GitRepository {
return GitRepository{
ID: r.ID.String(),
ProviderID: r.ProviderID.String(),
Name: r.Name,
FullName: r.FullName,
Description: r.Description,
CloneURL: r.CloneUrl,
WebhookURL: r.WebhookUrl,
DefaultBranch: r.DefaultBranch.String,
IsPrivate: r.IsPrivate.Bool,
UserID: r.UserID.String(),
CreatedAt: r.CreatedAt.Time,
UpdatedAt: r.UpdatedAt.Time,
}
}
func gitProviderFrom(p sqlcdb.ListGitProvidersRow) GitProvider {
return GitProvider{
ID: p.ID.String(),
Name: p.Name,
DisplayName: p.DisplayName,
APIUrl: p.ApiUrl,
WebhookUrl: p.WebhookUrl,
UserID: p.UserID.String(),
CreatedAt: p.CreatedAt.Time,
UpdatedAt: p.UpdatedAt.Time,
}
}
func handleGetGitProviders(c *gin.Context) { func handleGetGitProviders(c *gin.Context) {
ctx, ok := requireGitRequestContext(c) ctx, ok := requireGitRequestContext(c)
if !ok { if !ok {
return return
} }
userID, err := uuid.Parse(ctx.userID) rows, err := ctx.db.Query(`
if err != nil { SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) FROM git_providers
return WHERE user_id = $1
} ORDER BY created_at DESC
rows, err := sqlcdb.New(ctx.db.DB).ListGitProviders(context.Background(), userID) `, ctx.userID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
defer rows.Close()
providers := make([]GitProvider, 0, len(rows)) providers := make([]GitProvider, 0)
for _, p := range rows { for rows.Next() {
providers = append(providers, gitProviderFrom(p)) var provider GitProvider
if err := rows.Scan(
&provider.ID,
&provider.Name,
&provider.DisplayName,
&provider.APIUrl,
&provider.WebhookUrl,
&provider.UserID,
&provider.CreatedAt,
&provider.UpdatedAt,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
providers = append(providers, provider)
} }
c.JSON(http.StatusOK, gin.H{"providers": providers}) c.JSON(http.StatusOK, gin.H{"providers": providers})
@@ -207,22 +173,21 @@ func handleConnectGitHubApp(c *gin.Context) {
UserID: ctx.userID, UserID: ctx.userID,
} }
prow, err := sqlcdb.New(ctx.db.DB).UpsertGitHubAppProvider(context.Background(), sqlcdb.UpsertGitHubAppProviderParams{ if err := ctx.db.QueryRow(`
ID: uuid.MustParse(provider.ID), INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
Name: provider.Name, VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
DisplayName: provider.DisplayName, ON CONFLICT (name, user_id)
ApiUrl: provider.APIUrl, DO UPDATE SET
WebhookUrl: provider.WebhookUrl, display_name = EXCLUDED.display_name,
AccessToken: provider.AccessToken, api_url = EXCLUDED.api_url,
UserID: uuid.MustParse(provider.UserID), webhook_url = EXCLUDED.webhook_url,
}) access_token = EXCLUDED.access_token,
if err != nil { updated_at = NOW()
RETURNING id, created_at, updated_at
`, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl, provider.WebhookUrl, provider.AccessToken, provider.UserID).Scan(&provider.ID, &provider.CreatedAt, &provider.UpdatedAt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect GitHub App installation"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect GitHub App installation"})
return return
} }
provider.ID = prow.ID.String()
provider.CreatedAt = prow.CreatedAt.Time
provider.UpdatedAt = prow.UpdatedAt.Time
provider.AccessToken = "" provider.AccessToken = ""
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -294,16 +259,10 @@ func handleCreateGitProvider(c *gin.Context) {
return return
} }
q := sqlcdb.New(ctx.db.DB) if _, err := ctx.db.Exec(`
if err := q.InsertGitProvider(context.Background(), sqlcdb.InsertGitProviderParams{ INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
ID: uuid.MustParse(provider.ID), VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
Name: provider.Name, `, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl, provider.WebhookUrl, provider.AccessToken, provider.UserID); err != nil {
DisplayName: provider.DisplayName,
ApiUrl: provider.APIUrl,
WebhookUrl: provider.WebhookUrl,
AccessToken: provider.AccessToken,
UserID: uuid.MustParse(provider.UserID),
}); err != nil {
if isUniqueConstraintError(err) { if isUniqueConstraintError(err) {
c.JSON(http.StatusConflict, gin.H{"error": "Provider already connected for this account"}) c.JSON(http.StatusConflict, gin.H{"error": "Provider already connected for this account"})
return return
@@ -312,12 +271,13 @@ func handleCreateGitProvider(c *gin.Context) {
return return
} }
if ts, err := q.GetGitProviderTimestamps(context.Background(), uuid.MustParse(provider.ID)); err != nil { if err := ctx.db.QueryRow(`
SELECT created_at, updated_at
FROM git_providers
WHERE id = $1
`, provider.ID).Scan(&provider.CreatedAt, &provider.UpdatedAt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read created provider"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read created provider"})
return return
} else {
provider.CreatedAt = ts.CreatedAt.Time
provider.UpdatedAt = ts.UpdatedAt.Time
} }
provider.AccessToken = "" provider.AccessToken = ""
@@ -336,11 +296,12 @@ func handleGetGitRepositories(c *gin.Context) {
return return
} }
q := sqlcdb.New(ctx.db.DB) var providerName, providerDisplayName, providerAccessToken, providerAPIURL string
provRow, err := q.GetGitProviderForRepos(context.Background(), sqlcdb.GetGitProviderForReposParams{ err := ctx.db.QueryRow(`
ID: uuid.MustParse(providerID), SELECT name, display_name, access_token, api_url
UserID: uuid.MustParse(ctx.userID), FROM git_providers
}) WHERE id = $1 AND user_id = $2
`, providerID, ctx.userID).Scan(&providerName, &providerDisplayName, &providerAccessToken, &providerAPIURL)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
@@ -349,10 +310,6 @@ func handleGetGitRepositories(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
providerName := provRow.Name
providerDisplayName := provRow.DisplayName
providerAccessToken := provRow.AccessToken
providerAPIURL := provRow.ApiUrl
if c.DefaultQuery("sync", "1") != "0" { if c.DefaultQuery("sync", "1") != "0" {
if syncErr := syncProviderRepositories(ctx.db, providerID, ctx.userID, providerName, providerAPIURL, providerAccessToken); syncErr != nil { if syncErr := syncProviderRepositories(ctx.db, providerID, ctx.userID, providerName, providerAPIURL, providerAccessToken); syncErr != nil {
@@ -367,30 +324,53 @@ func handleGetGitRepositories(c *gin.Context) {
} }
offset := (page - 1) * limit offset := (page - 1) * limit
search := strings.TrimSpace(c.Query("search")) search := strings.TrimSpace(c.Query("search"))
searchNullable := sql.NullString{String: search, Valid: search != ""}
repoRows, err := q.ListGitRepositories(context.Background(), sqlcdb.ListGitRepositoriesParams{ rows, err := ctx.db.Query(`
ProviderID: uuid.MustParse(providerID), SELECT id, provider_id, name, full_name, COALESCE(description, ''), clone_url, COALESCE(webhook_url, ''),
UserID: uuid.MustParse(ctx.userID), default_branch, is_private, user_id, created_at, updated_at
Column3: search, FROM git_repositories
Limit: int32(limit), WHERE provider_id = $1 AND user_id = $2
Offset: int32(offset), AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'))
}) ORDER BY updated_at DESC
LIMIT $4 OFFSET $5
`, providerID, ctx.userID, searchNullable, limit, offset)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
defer rows.Close()
repositories := make([]GitRepository, 0, len(repoRows)) repositories := make([]GitRepository, 0)
for _, r := range repoRows { for rows.Next() {
repositories = append(repositories, gitRepoFrom(r)) var repo GitRepository
if err := rows.Scan(
&repo.ID,
&repo.ProviderID,
&repo.Name,
&repo.FullName,
&repo.Description,
&repo.CloneURL,
&repo.WebhookURL,
&repo.DefaultBranch,
&repo.IsPrivate,
&repo.UserID,
&repo.CreatedAt,
&repo.UpdatedAt,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
repositories = append(repositories, repo)
} }
total, err := q.CountGitRepositories(context.Background(), sqlcdb.CountGitRepositoriesParams{ var total int
ProviderID: uuid.MustParse(providerID), if err := ctx.db.QueryRow(`
UserID: uuid.MustParse(ctx.userID), SELECT COUNT(*)
Column3: search, FROM git_repositories
}) WHERE provider_id = $1 AND user_id = $2
if err != nil { AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'))
`, providerID, ctx.userID, searchNullable).Scan(&total); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
@@ -428,11 +408,12 @@ func handleConnectGitRepository(c *gin.Context) {
return return
} }
q := sqlcdb.New(ctx.db.DB) var providerName string
providerName, err := q.GetGitProviderName(context.Background(), sqlcdb.GetGitProviderNameParams{ err := ctx.db.QueryRow(`
ID: uuid.MustParse(providerID), SELECT name
UserID: uuid.MustParse(ctx.userID), FROM git_providers
}) WHERE id = $1 AND user_id = $2
`, providerID, ctx.userID).Scan(&providerName)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
@@ -448,16 +429,31 @@ func handleConnectGitRepository(c *gin.Context) {
return return
} }
existingRow, err := q.GetGitRepositoryByFullName(context.Background(), sqlcdb.GetGitRepositoryByFullNameParams{ var existing GitRepository
ProviderID: uuid.MustParse(providerID), err = ctx.db.QueryRow(`
FullName: fullName, SELECT id, provider_id, name, full_name, COALESCE(description, ''), clone_url, COALESCE(webhook_url, ''),
UserID: uuid.MustParse(ctx.userID), default_branch, is_private, user_id, created_at, updated_at
}) FROM git_repositories
WHERE provider_id = $1 AND full_name = $2 AND user_id = $3
`, providerID, fullName, ctx.userID).Scan(
&existing.ID,
&existing.ProviderID,
&existing.Name,
&existing.FullName,
&existing.Description,
&existing.CloneURL,
&existing.WebhookURL,
&existing.DefaultBranch,
&existing.IsPrivate,
&existing.UserID,
&existing.CreatedAt,
&existing.UpdatedAt,
)
switch { switch {
case err == nil: case err == nil:
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "Repository already connected", "message": "Repository already connected",
"repository": gitRepoFromFullNameRow(existingRow), "repository": existing,
}) })
return return
case !errors.Is(err, sql.ErrNoRows): case !errors.Is(err, sql.ErrNoRows):
@@ -477,18 +473,13 @@ func handleConnectGitRepository(c *gin.Context) {
UserID: ctx.userID, UserID: ctx.userID,
} }
ins, err := q.InsertGitRepository(context.Background(), sqlcdb.InsertGitRepositoryParams{ if err := ctx.db.QueryRow(`
ID: uuid.MustParse(repo.ID), INSERT INTO git_repositories (
ProviderID: uuid.MustParse(repo.ProviderID), id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at
Name: repo.Name, )
FullName: repo.FullName, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
Description: sql.NullString{String: repo.Description, Valid: true}, RETURNING created_at, updated_at
CloneUrl: repo.CloneURL, `, repo.ID, repo.ProviderID, repo.Name, repo.FullName, repo.Description, repo.CloneURL, repo.DefaultBranch, repo.IsPrivate, repo.UserID).Scan(&repo.CreatedAt, &repo.UpdatedAt); err != nil {
DefaultBranch: sql.NullString{String: repo.DefaultBranch, Valid: true},
IsPrivate: sql.NullBool{Bool: repo.IsPrivate, Valid: true},
UserID: uuid.MustParse(repo.UserID),
})
if err != nil {
if isUniqueConstraintError(err) { if isUniqueConstraintError(err) {
c.JSON(http.StatusConflict, gin.H{"error": "Repository is already connected"}) c.JSON(http.StatusConflict, gin.H{"error": "Repository is already connected"})
return return
@@ -496,8 +487,6 @@ func handleConnectGitRepository(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"})
return return
} }
repo.CreatedAt = ins.CreatedAt.Time
repo.UpdatedAt = ins.UpdatedAt.Time
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{
"message": "Repository connected successfully", "message": "Repository connected successfully",
@@ -527,10 +516,12 @@ func handleCreateWebhook(c *gin.Context) {
return return
} }
providerUUID, err := sqlcdb.New(ctx.db.DB).GetGitRepositoryProviderID(context.Background(), sqlcdb.GetGitRepositoryProviderIDParams{ var providerID string
ID: uuid.MustParse(repoID), err := ctx.db.QueryRow(`
UserID: uuid.MustParse(ctx.userID), SELECT provider_id
}) FROM git_repositories
WHERE id = $1 AND user_id = $2
`, repoID, ctx.userID).Scan(&providerID)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
@@ -539,7 +530,6 @@ func handleCreateWebhook(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
providerID := providerUUID.String()
eventsJSON, err := json.Marshal(req.Events) eventsJSON, err := json.Marshal(req.Events)
if err != nil { if err != nil {
@@ -562,22 +552,23 @@ func handleCreateWebhook(c *gin.Context) {
Active: true, Active: true,
} }
wrow, err := sqlcdb.New(ctx.db.DB).UpsertGitWebhook(context.Background(), sqlcdb.UpsertGitWebhookParams{ if err := ctx.db.QueryRow(`
ID: uuid.MustParse(webhook.ID), INSERT INTO git_webhooks (
RepoID: uuid.MustParse(webhook.RepoID), id, repo_id, provider_id, events, webhook_secret, active, branch_filter, created_at, updated_at
ProviderID: uuid.MustParse(webhook.ProviderID), )
Events: webhook.Events, VALUES ($1, $2, $3, $4, $5, TRUE, $6, NOW(), NOW())
WebhookSecret: webhook.Secret, ON CONFLICT (repo_id, provider_id)
BranchFilter: sql.NullString{String: strings.TrimSpace(req.Branch), Valid: true}, DO UPDATE SET
}) events = EXCLUDED.events,
if err != nil { webhook_secret = EXCLUDED.webhook_secret,
active = TRUE,
branch_filter = EXCLUDED.branch_filter,
updated_at = NOW()
RETURNING id, active, created_at, updated_at
`, webhook.ID, webhook.RepoID, webhook.ProviderID, webhook.Events, webhook.Secret, strings.TrimSpace(req.Branch)).Scan(&webhook.ID, &webhook.Active, &webhook.CreatedAt, &webhook.UpdatedAt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure webhook"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure webhook"})
return return
} }
webhook.ID = wrow.ID.String()
webhook.Active = wrow.Active.Bool
webhook.CreatedAt = wrow.CreatedAt.Time
webhook.UpdatedAt = wrow.UpdatedAt.Time
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{
"message": "Webhook configured successfully", "message": "Webhook configured successfully",
@@ -607,39 +598,69 @@ func handleGetConnectedRepositories(c *gin.Context) {
} }
offset := (page - 1) * limit offset := (page - 1) * limit
q := sqlcdb.New(ctx.db.DB) rows, err := ctx.db.Query(`
repoRows, err := q.ListConnectedRepositories(context.Background(), sqlcdb.ListConnectedRepositoriesParams{ SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.description, ''), r.clone_url,
UserID: uuid.MustParse(ctx.userID), r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at,
Limit: int32(limit), p.name as provider_name, p.display_name
Offset: int32(offset), FROM git_repositories r
}) JOIN git_providers p ON r.provider_id = p.id
WHERE r.user_id = $1
ORDER BY r.updated_at DESC
LIMIT $2 OFFSET $3
`, ctx.userID, limit, offset)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
defer rows.Close()
repositories := make([]map[string]interface{}, 0)
for rows.Next() {
var repo GitRepository
var providerName, providerDisplayName string
if err := rows.Scan(
&repo.ID,
&repo.ProviderID,
&repo.Name,
&repo.FullName,
&repo.Description,
&repo.CloneURL,
&repo.DefaultBranch,
&repo.IsPrivate,
&repo.UserID,
&repo.CreatedAt,
&repo.UpdatedAt,
&providerName,
&providerDisplayName,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
repositories := make([]map[string]interface{}, 0, len(repoRows))
for _, r := range repoRows {
repositories = append(repositories, map[string]interface{}{ repositories = append(repositories, map[string]interface{}{
"id": r.ID.String(), "id": repo.ID,
"provider_id": r.ProviderID.String(), "provider_id": repo.ProviderID,
"name": r.Name, "name": repo.Name,
"full_name": r.FullName, "full_name": repo.FullName,
"description": r.Description, "description": repo.Description,
"clone_url": r.CloneUrl, "clone_url": repo.CloneURL,
"default_branch": r.DefaultBranch.String, "default_branch": repo.DefaultBranch,
"is_private": r.IsPrivate.Bool, "is_private": repo.IsPrivate,
"created_at": r.CreatedAt.Time, "created_at": repo.CreatedAt,
"updated_at": r.UpdatedAt.Time, "updated_at": repo.UpdatedAt,
"provider": map[string]string{ "provider": map[string]string{
"name": r.ProviderName, "name": providerName,
"display_name": r.DisplayName, "display_name": providerDisplayName,
}, },
}) })
} }
total, err := q.CountUserRepositories(context.Background(), uuid.MustParse(ctx.userID)) var total int
if err != nil { if err := ctx.db.QueryRow(`
SELECT COUNT(*)
FROM git_repositories
WHERE user_id = $1
`, ctx.userID).Scan(&total); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
@@ -684,17 +705,20 @@ func syncProviderRepositories(db *database.DB, providerID, userID, providerName,
defaultBranch = "main" defaultBranch = "main"
} }
err := sqlcdb.New(db.DB).UpsertGitRepository(context.Background(), sqlcdb.UpsertGitRepositoryParams{ _, err := db.Exec(`
ID: uuid.New(), INSERT INTO git_repositories (
ProviderID: uuid.MustParse(providerID), id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at
Name: repository.Name, )
FullName: repository.FullName, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
Description: sql.NullString{String: repository.Description, Valid: true}, ON CONFLICT (provider_id, full_name)
CloneUrl: cloneURL, DO UPDATE SET
DefaultBranch: sql.NullString{String: defaultBranch, Valid: true}, name = EXCLUDED.name,
IsPrivate: sql.NullBool{Bool: repository.IsPrivate, Valid: true}, description = EXCLUDED.description,
UserID: uuid.MustParse(userID), clone_url = EXCLUDED.clone_url,
}) default_branch = EXCLUDED.default_branch,
is_private = EXCLUDED.is_private,
updated_at = NOW()
`, uuid.NewString(), providerID, repository.Name, repository.FullName, repository.Description, cloneURL, defaultBranch, repository.IsPrivate, userID)
if err != nil { if err != nil {
return err return err
} }
@@ -1299,15 +1323,16 @@ func handleDeleteGitProvider(c *gin.Context) {
return return
} }
affected, err := sqlcdb.New(ctx.db.DB).DeleteGitProvider(context.Background(), sqlcdb.DeleteGitProviderParams{ result, err := ctx.db.Exec(
ID: uuid.MustParse(providerID), "DELETE FROM git_providers WHERE id = $1 AND user_id = $2",
UserID: uuid.MustParse(ctx.userID), providerID, ctx.userID,
}) )
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"})
return return
} }
affected, _ := result.RowsAffected()
if affected == 0 { if affected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
return return
@@ -1337,10 +1362,12 @@ func handleGetGitRepositoryBranches(c *gin.Context) {
return return
} }
pf, err := sqlcdb.New(ctx.db.DB).GetGitProviderForFetch(context.Background(), sqlcdb.GetGitProviderForFetchParams{ var providerName, providerAPIURL, providerAccessToken string
ID: uuid.MustParse(providerID), err := ctx.db.QueryRow(`
UserID: uuid.MustParse(ctx.userID), SELECT name, api_url, access_token
}) FROM git_providers
WHERE id = $1 AND user_id = $2
`, providerID, ctx.userID).Scan(&providerName, &providerAPIURL, &providerAccessToken)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
@@ -1349,9 +1376,6 @@ func handleGetGitRepositoryBranches(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
providerName := pf.Name
providerAPIURL := pf.ApiUrl
providerAccessToken := pf.AccessToken
branches, err := fetchRepositoryBranches(providerName, providerAPIURL, providerAccessToken, owner, repo) branches, err := fetchRepositoryBranches(providerName, providerAPIURL, providerAccessToken, owner, repo)
if err != nil { if err != nil {
+64 -69
View File
@@ -5,7 +5,6 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"crypto/subtle" "crypto/subtle"
"database/sql"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -15,7 +14,6 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
@@ -40,15 +38,22 @@ func handleGitWebhookPush(c *gin.Context) {
return return
} }
q := sqlcdb.New(db.DB) var (
hook, err := q.GetGitWebhookForPush(context.Background(), uuid.MustParse(webhookID)) secret string
repoID string
providerID string
branchFilter string
active bool
)
err := db.QueryRow(`
SELECT webhook_secret, repo_id, provider_id, COALESCE(branch_filter, ''), active
FROM git_webhooks WHERE id = $1`, webhookID).
Scan(&secret, &repoID, &providerID, &branchFilter, &active)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"})
return return
} }
secret := hook.WebhookSecret if !active {
branchFilter := hook.BranchFilter
if !hook.Active.Bool {
c.JSON(http.StatusForbidden, gin.H{"error": "Webhook is disabled"}) c.JSON(http.StatusForbidden, gin.H{"error": "Webhook is disabled"})
return return
} }
@@ -59,7 +64,8 @@ func handleGitWebhookPush(c *gin.Context) {
return return
} }
providerName, _ := q.GetGitProviderNameByID(context.Background(), hook.ProviderID) var providerName string
_ = db.QueryRow(`SELECT name FROM git_providers WHERE id = $1`, providerID).Scan(&providerName)
if !verifyGitWebhookSignature(providerName, secret, c.Request, body) { if !verifyGitWebhookSignature(providerName, secret, c.Request, body) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"})
@@ -112,12 +118,14 @@ func handleGitWebhookPush(c *gin.Context) {
commit = payload.After commit = payload.After
} }
repo, err := q.GetGitRepoForPush(context.Background(), hook.RepoID) var cloneURL, fullName, repoUserID string
err = db.QueryRow(
`SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1`, repoID,
).Scan(&cloneURL, &fullName, &repoUserID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
return return
} }
cloneURL, fullName, repoUserID := repo.CloneUrl, repo.FullName, repo.UserID.String()
engineValue, _ := c.Get("deployment_engine") engineValue, _ := c.Get("deployment_engine")
engine, _ := engineValue.(*deployment.DeploymentEngine) engine, _ := engineValue.(*deployment.DeploymentEngine)
@@ -139,37 +147,34 @@ func handleGitWebhookPush(c *gin.Context) {
// the pushed repo+branch. Shared by the per-repo webhook receiver and the // the pushed repo+branch. Shared by the per-repo webhook receiver and the
// GitHub App webhook endpoint. // GitHub App webhook endpoint.
func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.DeploymentEngine, cloneURL, fullName, branch, commit, repoUserID string) (int, error) { func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.DeploymentEngine, cloneURL, fullName, branch, commit, repoUserID string) (int, error) {
q := sqlcdb.New(db.DB) // Sync-style events may carry no branch — match services on repo only.
ctx := context.Background() branchClause := "s.git_branch = $1"
// Sync-style events may carry no branch — empty branch matches all. if branch == "" {
rows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{ branchClause = "TRUE"
GitBranch: sql.NullString{String: branch, Valid: true}, }
GitRepo: sql.NullString{String: cloneURL, Valid: true}, rows, err := db.Query(fmt.Sprintf(`
GitRepo_2: sql.NullString{String: fullName, Valid: true}, 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,
s.created_at, s.updated_at
FROM services s
WHERE %s AND (s.git_repo = $2 OR s.git_repo = $3)`, branchClause),
branch, cloneURL, fullName)
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer rows.Close()
var services []Service var services []Service
for _, row := range rows { for rows.Next() {
services = append(services, Service{ var s Service
ID: row.ID, if err := rows.Scan(
ProjectID: row.ProjectID, &s.ID, &s.ProjectID, &s.Name, &s.Type, &s.Status, &s.Image, &s.Command,
Name: row.Name, &s.Environment, &s.GitRepo, &s.GitBranch, &s.BuildPath, &s.CPU, &s.Memory,
Type: row.Type, &s.CreatedAt, &s.UpdatedAt,
Status: row.Status, ); err != nil {
Image: row.Image, return 0, err
Command: row.Command, }
Environment: row.Environment, services = append(services, s)
GitRepo: row.GitRepo,
GitBranch: row.GitBranch,
BuildPath: row.BuildPath,
CPU: row.Cpu,
Memory: row.Memory,
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
})
} }
enqueued := 0 enqueued := 0
@@ -187,34 +192,27 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
if err := q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{ if _, err := db.Exec(
ID: d.ID, `INSERT INTO deployments
ServiceID: d.ServiceID, (id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
Version: fmt.Sprintf("v%d", now.Unix()), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
CommitHash: sql.NullString{String: ptrStr(d.CommitHash), Valid: d.CommitHash != nil}, d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt,
Status: sql.NullString{String: d.Status, Valid: true}, ); err != nil {
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},
}); err != nil {
continue continue
} }
if engine == nil { if engine == nil {
failure := "Deployment engine unavailable. Docker may not be configured on this server."
failedAt := time.Now() failedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ failure := "Deployment engine unavailable. Docker may not be configured on this server."
Error: sql.NullString{String: failure, Valid: true}, _, _ = db.Exec(
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
ID: d.ID, failure, failedAt, d.ID,
}) )
continue continue
} }
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, _ = db.Exec(
Status: sql.NullString{String: "building", Valid: true}, `UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, time.Now(), service.ID,
ID: service.ID, )
})
// Webhook pushes default to a clean build — stale layers are the // Webhook pushes default to a clean build — stale layers are the
// classic silent-rollback failure (dflow convention). // classic silent-rollback failure (dflow convention).
branchForBuild := branch branchForBuild := branch
@@ -228,17 +226,14 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.
runDeploymentAndSync(jctx, db, engine, &d, service, webhookReq, repoUserID) runDeploymentAndSync(jctx, db, engine, &d, service, webhookReq, repoUserID)
}, },
}); pos > 0 { }); pos > 0 {
now := sql.NullTime{Time: time.Now(), Valid: true} _, _ = db.Exec(
_ = q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{ `UPDATE deployments SET status = 'queued', updated_at = $1 WHERE id = $2`,
Status: sql.NullString{String: "queued", Valid: true}, time.Now(), d.ID,
UpdatedAt: now, )
ID: d.ID, _, _ = db.Exec(
}) `UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`,
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ time.Now(), service.ID,
Status: sql.NullString{String: "queued", Valid: true}, )
UpdatedAt: now,
ID: service.ID,
})
} }
enqueued++ enqueued++
} }
+1 -203
View File
@@ -1,16 +1,6 @@
package api package api
import ( import "testing"
"context"
"database/sql"
"strings"
"testing"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
)
func TestNormalizeRepositoryFullName(t *testing.T) { func TestNormalizeRepositoryFullName(t *testing.T) {
cases := []struct { cases := []struct {
@@ -93,195 +83,3 @@ func TestDeriveCloneURL(t *testing.T) {
}) })
} }
} }
// Live invariant test against real Postgres — set METRICS_TEST_DSN (or
// DATABASE_URL). Covers the generated git provider/repo/webhook queries plus
// the push-dispatch service matcher (empty branch must match all branches).
func TestGitLiveInvariants(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()
providerID := uuid.New()
repoID := uuid.New()
webhookID := uuid.New()
svcMain := uuid.New()
svcDev := uuid.New()
suffix := strings.ReplaceAll(userID.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, "git-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "git-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }()
// Provider lifecycle.
must(q.InsertGitProvider(ctx, sqlcdb.InsertGitProviderParams{
ID: providerID, Name: "github", DisplayName: "GitHub",
ApiUrl: "https://api.github.com", WebhookUrl: "https://hooks.test",
AccessToken: "tok", UserID: userID,
}))
providers, err := q.ListGitProviders(ctx, userID)
if err != nil || len(providers) == 0 {
t.Fatalf("providers: %v %v", err, providers)
}
name, err := q.GetGitProviderNameByID(ctx, providerID)
if err != nil || name != "github" {
t.Fatalf("provider name: %v %q", err, name)
}
fetch, err := q.GetGitProviderForFetch(ctx, sqlcdb.GetGitProviderForFetchParams{ID: providerID, UserID: userID})
if err != nil || fetch.AccessToken != "tok" || fetch.ApiUrl != "https://api.github.com" {
t.Fatalf("fetch: %v %+v", err, fetch)
}
// Repository insert + search.
cloneURL := "https://github.com/acme/svc-" + suffix + ".git"
fullName := "acme/svc-" + suffix
_, err = q.InsertGitRepository(ctx, sqlcdb.InsertGitRepositoryParams{
ID: repoID, ProviderID: providerID, Name: "svc-" + suffix, FullName: fullName,
Description: sql.NullString{String: "", Valid: true},
CloneUrl: cloneURL,
DefaultBranch: sql.NullString{String: "main", Valid: true},
IsPrivate: sql.NullBool{Bool: false, Valid: true},
UserID: userID,
})
must(err)
byName, err := q.GetGitRepositoryByFullName(ctx, sqlcdb.GetGitRepositoryByFullNameParams{
ProviderID: providerID, FullName: fullName, UserID: userID,
})
if err != nil || byName.ID != repoID || byName.CloneUrl != cloneURL {
t.Fatalf("repo by full name: %v %+v", err, byName)
}
all, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{
ProviderID: providerID, UserID: userID, Column3: "", Limit: 50, Offset: 0,
})
if err != nil || len(all) == 0 {
t.Fatalf("repo list (empty search): %v %v", err, all)
}
hit, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{
ProviderID: providerID, UserID: userID, Column3: "svc-" + suffix, Limit: 50, Offset: 0,
})
if err != nil || len(hit) != 1 || hit[0].ID != repoID {
t.Fatalf("repo list (matching search): %v %v", err, hit)
}
miss, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{
ProviderID: providerID, UserID: userID, Column3: "zzz-no-match", Limit: 50, Offset: 0,
})
if err != nil || len(miss) != 0 {
t.Fatalf("repo list (non-matching search): %v %v", err, miss)
}
count, err := q.CountGitRepositories(ctx, sqlcdb.CountGitRepositoriesParams{
ProviderID: providerID, UserID: userID, Column3: "",
})
if err != nil || count < 1 {
t.Fatalf("repo count: %v %d", err, count)
}
// Upsert on (provider_id, full_name) conflict — updates rather than errors.
must(q.UpsertGitRepository(ctx, sqlcdb.UpsertGitRepositoryParams{
ID: uuid.New(), ProviderID: providerID, Name: "renamed-" + suffix, FullName: fullName,
Description: sql.NullString{String: "d", Valid: true},
CloneUrl: cloneURL,
DefaultBranch: sql.NullString{String: "main", Valid: true},
IsPrivate: sql.NullBool{Bool: true, Valid: true},
UserID: userID,
}))
byName, err = q.GetGitRepositoryByFullName(ctx, sqlcdb.GetGitRepositoryByFullNameParams{
ProviderID: providerID, FullName: fullName, UserID: userID,
})
if err != nil || byName.Name != "renamed-"+suffix {
t.Fatalf("repo after upsert: %v %+v", err, byName)
}
// Connected-repos join + webhook lifecycle.
connected, err := q.ListConnectedRepositories(ctx, sqlcdb.ListConnectedRepositoriesParams{
UserID: userID, Limit: 50, Offset: 0,
})
if err != nil || len(connected) == 0 {
t.Fatalf("connected repos: %v %v", err, connected)
}
wh, err := q.UpsertGitWebhook(ctx, sqlcdb.UpsertGitWebhookParams{
ID: webhookID, RepoID: repoID, ProviderID: providerID,
Events: "push", WebhookSecret: "sec-" + suffix,
BranchFilter: sql.NullString{String: "", Valid: true},
})
must(err)
if !wh.Active.Bool {
t.Fatalf("webhook inactive after upsert: %+v", wh)
}
push, err := q.GetGitWebhookForPush(ctx, webhookID)
if err != nil || push.WebhookSecret != "sec-"+suffix || push.ProviderID != providerID {
t.Fatalf("webhook for push: %v %+v", err, push)
}
pushRepo, err := q.GetGitRepoForPush(ctx, repoID)
if err != nil || pushRepo.CloneUrl != cloneURL || pushRepo.FullName != fullName || pushRepo.UserID != userID {
t.Fatalf("repo for push: %v %+v", err, pushRepo)
}
// Push-dispatch service matcher: branch filter and empty-branch match-all.
for id, branch := range map[uuid.UUID]string{svcMain: "main", svcDev: "dev"} {
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type, git_repo, git_branch)
VALUES ($1, $2, $3, $4, 'web', 'git', $5, $6)`,
id, "svc-"+suffix+"-"+branch, projectID, envID, cloneURL, branch)
must(err)
}
servicesFor := func(branch string) []sqlcdb.ListServicesForPushRow {
rows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{
GitBranch: sql.NullString{String: branch, Valid: true},
GitRepo: sql.NullString{String: cloneURL, Valid: true},
GitRepo_2: sql.NullString{String: fullName, Valid: true},
})
if err != nil {
t.Fatalf("services for push %q: %v", branch, err)
}
return rows
}
mainRows := servicesFor("main")
if len(mainRows) != 1 || mainRows[0].ID != svcMain || mainRows[0].GitBranch != "main" {
t.Fatalf("main branch dispatch: %+v", mainRows)
}
devRows := servicesFor("dev")
if len(devRows) != 1 || devRows[0].ID != svcDev {
t.Fatalf("dev branch dispatch: %+v", devRows)
}
allRows := servicesFor("")
if len(allRows) != 2 {
t.Fatalf("empty-branch dispatch should match all: %+v", allRows)
}
// Repo may also be referenced by its full_name.
fullRows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{
GitBranch: sql.NullString{String: "main", Valid: true},
GitRepo: sql.NullString{String: "[email protected]:other/none.git", Valid: true},
GitRepo_2: sql.NullString{String: cloneURL, Valid: true},
})
if err != nil || len(fullRows) != 1 {
t.Fatalf("full-name repo match: %v %+v", err, fullRows)
}
// Provider delete is owner-scoped.
other, err := q.DeleteGitProvider(ctx, sqlcdb.DeleteGitProviderParams{ID: providerID, UserID: uuid.New()})
if err != nil || other != 0 {
t.Fatalf("foreign delete affected %d rows: %v", other, err)
}
deleted, err := q.DeleteGitProvider(ctx, sqlcdb.DeleteGitProviderParams{ID: providerID, UserID: userID})
if err != nil || deleted != 1 {
t.Fatalf("owner delete affected %d rows: %v", deleted, err)
}
}
+279 -235
View File
@@ -40,68 +40,6 @@ 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"`
@@ -148,21 +86,51 @@ 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 := sqlcdb.New(db.(*database.DB).DB).ListPreviewEnvironmentsForProject(c.Request.Context(), projectID) rows, err := db.(*database.DB).Query(
`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()
environments := make([]PreviewEnvironment, 0, len(rows)) var environments []PreviewEnvironment
for _, r := range rows { for rows.Next() {
environments = append(environments, previewFromRow(previewRowShape{ var env PreviewEnvironment
ID: r.ID, ProjectID: r.ProjectID, ServiceID: r.ServiceID, var serviceID sql.NullString
PreviewServiceID: r.PreviewServiceID, BranchName: r.BranchName, var serviceName sql.NullString
PrNumber: r.PrNumber, Environment: r.Environment, Status: r.Status, var serviceType sql.NullString
Url: r.Url, ExpiresAt: r.ExpiresAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
SvcID: r.SvcID, ServiceName: r.ServiceName, ServiceType: r.ServiceType, err := rows.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 {
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})
@@ -203,37 +171,48 @@ func handleCreatePreviewEnvironment(c *gin.Context) {
return return
} }
ctx := c.Request.Context()
q := sqlcdb.New(db.(*database.DB).DB)
// Check if project exists and user has access // Check if project exists and user has access
project, err := q.GetProjectBrief(ctx, req.ProjectID) var project Project
err = db.(*database.DB).QueryRow(
"SELECT id, name, owner_id FROM projects WHERE id = $1",
req.ProjectID,
).Scan(&project.ID, &project.Name, &project.OwnerID)
if err != nil { 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.String() != userID.(string) && !contextIsAdmin(c) { if project.OwnerID != 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
service, err := q.GetServiceTypeBrief(ctx, sqlcdb.GetServiceTypeBriefParams{ var service Service
ID: req.ServiceID, ProjectID: req.ProjectID}) err = db.(*database.DB).QueryRow(
"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
count, err := q.CountActivePreviewsForBranch(ctx, sqlcdb.CountActivePreviewsForBranchParams{ var count int
ServiceID: req.ServiceID, BranchName: req.BranchName}) err = db.(*database.DB).QueryRow(
"SELECT COUNT(*) FROM preview_environments WHERE service_id = $1 AND branch_name = $2 AND status NOT IN ('expired', 'stopped')",
req.ServiceID, req.BranchName,
).Scan(&count)
if err != nil { 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
@@ -258,23 +237,29 @@ 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 := q.UpdateServicePreviewBranch(ctx, sqlcdb.UpdateServicePreviewBranchParams{ if _, err := db.(*database.DB).Exec(
GitBranch: sql.NullString{String: req.BranchName, Valid: true}, ID: cloneID}); err != nil { `UPDATE services SET git_branch = $1, domain = '', updated_at = NOW() WHERE id = $2`,
_ = q.DeleteServiceByID(ctx, cloneID) req.BranchName, 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 := q.DeleteAllServiceDomains(ctx, cloneID); err != nil { if _, err := db.(*database.DB).Exec(
_ = q.DeleteServiceByID(ctx, cloneID) `DELETE FROM service_domains WHERE service_id = $1`, 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 != "" {
_ = q.CreatePreviewDomain(ctx, sqlcdb.CreatePreviewDomainParams{ _, _ = db.(*database.DB).Exec(
ServiceID: cloneID, Domain: previewHost}) `INSERT INTO service_domains (service_id, domain, is_default, cert_type) VALUES ($1, $2, true, 'tls')`,
cloneID, previewHost,
)
} }
expiresAt := time.Now().Add(time.Duration(ttlHours) * time.Hour) expiresAt := time.Now().Add(time.Duration(ttlHours) * time.Hour)
@@ -294,36 +279,30 @@ func handleCreatePreviewEnvironment(c *gin.Context) {
env.URL = "https://" + previewHost env.URL = "https://" + previewHost
} }
err = q.CreatePreviewEnvironment(ctx, sqlcdb.CreatePreviewEnvironmentParams{ _, err = db.(*database.DB).Exec(
ID: env.ID, `INSERT INTO preview_environments
ProjectID: env.ProjectID, (id, project_id, service_id, preview_service_id, branch_name, pr_number, environment,
ServiceID: env.ServiceID, status, url, expires_at, created_at, updated_at)
PreviewServiceID: uuid.NullUUID{UUID: cloneID, Valid: true}, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
BranchName: env.BranchName, env.ID, env.ProjectID, env.ServiceID, cloneID, env.BranchName, env.PRNumber,
PrNumber: prNumberSQL(env.PRNumber), env.Environment, env.Status, env.URL, env.ExpiresAt, env.CreatedAt, env.UpdatedAt,
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 {
_ = q.DeleteServiceByID(ctx, cloneID) _, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, 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 = q.InsertPendingDeployment(ctx, sqlcdb.InsertPendingDeploymentParams{ _, err = db.(*database.DB).Exec(
ID: deploymentID, `INSERT INTO deployments
ServiceID: cloneID, (id, service_id, version, commit_hash, status, created_at, updated_at)
Version: "preview-" + env.Environment, VALUES ($1, $2, $3, $4, 'pending', NOW(), NOW())`,
CommitHash: sql.NullString{String: req.BranchName, Valid: true}, deploymentID, cloneID, "preview-"+env.Environment, req.BranchName,
}) )
if err != nil { if err != nil {
_ = q.DeletePreviewEnvironment(ctx, env.ID) _, _ = db.(*database.DB).Exec(`DELETE FROM preview_environments WHERE id = $1`, env.ID)
_ = q.DeleteServiceByID(ctx, cloneID) _, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, 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
} }
@@ -370,20 +349,43 @@ 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)
r, err := sqlcdb.New(db.(*database.DB).DB).GetPreviewEnvironment( var env PreviewEnvironment
c.Request.Context(), sqlcdb.GetPreviewEnvironmentParams{ var serviceID sql.NullString
ID: envID, OwnerID: userID, Column3: contextIsAdmin(c)}) var serviceName sql.NullString
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{
ID: r.ID, ProjectID: r.ProjectID, ServiceID: r.ServiceID, // Populate service info if available
PreviewServiceID: r.PreviewServiceID, BranchName: r.BranchName, if serviceID.Valid {
PrNumber: r.PrNumber, Environment: r.Environment, Status: r.Status, parsedServiceID, parseErr := uuid.Parse(serviceID.String)
Url: r.Url, ExpiresAt: r.ExpiresAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, if parseErr == nil {
SvcID: r.SvcID, ServiceName: r.ServiceName, ServiceType: r.ServiceType, env.Service = &Service{
}) 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})
} }
@@ -416,21 +418,24 @@ func handleUpdatePreviewEnvironment(c *gin.Context) {
} }
// Check if preview environment exists and user has access // Check if preview environment exists and user has access
uid, _ := uuid.Parse(fmt.Sprint(userID)) var existingEnv PreviewEnvironment
q := sqlcdb.New(db.(*database.DB).DB) err = db.(*database.DB).QueryRow(
r, err := q.GetPreviewEnvironmentForWrite( `SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.pr_number,
c.Request.Context(), sqlcdb.GetPreviewEnvironmentForWriteParams{ pe.environment, pe.status, pe.url, pe.expires_at, pe.created_at, pe.updated_at
ID: envID, OwnerID: uid, Column3: contextIsAdmin(c)}) 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(
&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 != "" {
@@ -450,17 +455,12 @@ func handleUpdatePreviewEnvironment(c *gin.Context) {
existingEnv.UpdatedAt = time.Now() existingEnv.UpdatedAt = time.Now()
// Update preview environment in database // Update preview environment in database
expiresAt := time.Time{} _, err = db.(*database.DB).Exec(
if existingEnv.ExpiresAt != nil { `UPDATE preview_environments
expiresAt = *existingEnv.ExpiresAt SET status = $1, url = $2, expires_at = $3, updated_at = $4
} WHERE id = $5`,
err = q.UpdatePreviewEnvironment(c.Request.Context(), sqlcdb.UpdatePreviewEnvironmentParams{ existingEnv.Status, existingEnv.URL, existingEnv.ExpiresAt, existingEnv.UpdatedAt, existingEnv.ID,
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,34 +492,46 @@ func handleDeletePreviewEnvironment(c *gin.Context) {
return return
} }
ctx := c.Request.Context()
q := sqlcdb.New(db.(*database.DB).DB)
// Check if preview environment exists and user has access // Check if preview environment exists and user has access
projectOwnerID, err := q.GetPreviewEnvironmentOwner(ctx, envID) var projectOwnerID string
err = db.(*database.DB).QueryRow(
`SELECT p.owner_id
FROM preview_environments pe
JOIN projects p ON pe.project_id = p.id
WHERE pe.id = $1`,
envID,
).Scan(&projectOwnerID)
if err != nil { 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.String() != userID.(string) && !contextIsAdmin(c) { if projectOwnerID != 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.
if cloneID, err := q.GetPreviewServiceID(ctx, envID); err == nil && cloneID.Valid { var cloneID *uuid.UUID
_ = 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(ctx, db.(*database.DB), engine, cloneID.UUID) removeServiceRuntime(c.Request.Context(), db.(*database.DB), engine, *cloneID)
} }
} }
_ = q.DeleteServiceByID(ctx, cloneID.UUID) _, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, *cloneID)
} }
// Delete preview environment // Delete preview environment
err = q.DeletePreviewEnvironment(ctx, envID) _, err = db.(*database.DB).Exec(
"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"})
@@ -557,30 +569,22 @@ func handlePromotePreviewEnvironment(c *gin.Context) {
return return
} }
ctx := c.Request.Context()
q := sqlcdb.New(db.(*database.DB).DB)
// Get preview environment details // Get preview environment details
uid, _ := uuid.Parse(fmt.Sprint(userID)) var env PreviewEnvironment
r, err := q.GetPreviewEnvironmentForPromote( err = db.(*database.DB).QueryRow(
ctx, sqlcdb.GetPreviewEnvironmentForPromoteParams{ `SELECT pe.id, pe.project_id, pe.service_id, pe.preview_service_id, pe.branch_name, pe.environment, pe.status
ID: envID, OwnerID: uid, Column3: contextIsAdmin(c)}) 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,
)
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" {
@@ -592,12 +596,11 @@ 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 = q.InsertPendingDeployment(ctx, sqlcdb.InsertPendingDeploymentParams{ _, err = db.(*database.DB).Exec(
ID: deploymentID, `INSERT INTO deployments (id, service_id, version, commit_hash, status, created_at, updated_at)
ServiceID: env.ServiceID, VALUES ($1, $2, $3, $4, 'pending', NOW(), NOW())`,
Version: promotionVersion, deploymentID, env.ServiceID, promotionVersion, env.BranchName,
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
@@ -625,14 +628,18 @@ 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(ctx, db.(*database.DB), engine, *env.PreviewServiceID) removeServiceRuntime(c.Request.Context(), db.(*database.DB), engine, *env.PreviewServiceID)
} }
} }
_ = q.DeleteServiceByID(ctx, *env.PreviewServiceID) _, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, *env.PreviewServiceID)
} }
if err := q.MarkPreviewEnvironmentStatus(ctx, sqlcdb.MarkPreviewEnvironmentStatusParams{ if _, err := db.(*database.DB).Exec(
Status: "stopped", ID: env.ID}); err != nil { `UPDATE preview_environments
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
} }
@@ -675,27 +682,28 @@ 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 := q.ListExpiredPreviewsForUser(ctx, sqlcdb.ListExpiredPreviewsForUserParams{ rows, err := db.(*database.DB).Query(
OwnerID: uid, Column2: contextIsAdmin(c)}) `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'`,
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()
expiredEnvs := make([]PreviewEnvironment, 0, len(rows)) var expiredEnvs []PreviewEnvironment
for _, r := range rows { for rows.Next() {
env := PreviewEnvironment{ var env PreviewEnvironment
ID: r.ID, ProjectID: r.ProjectID, ServiceID: r.ServiceID, err := rows.Scan(
BranchName: r.BranchName, Environment: r.Environment, &env.ID, &env.ProjectID, &env.ServiceID, &env.PreviewServiceID, &env.BranchName, &env.Environment,
} )
if r.PreviewServiceID.Valid { if err != nil {
id := r.PreviewServiceID.UUID continue
env.PreviewServiceID = &id
} }
expiredEnvs = append(expiredEnvs, env) expiredEnvs = append(expiredEnvs, env)
} }
@@ -703,17 +711,20 @@ 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 {
if err := q.MarkPreviewEnvironmentStatus(ctx, sqlcdb.MarkPreviewEnvironmentStatusParams{ _, err := db.(*database.DB).Exec(
Status: "expired", ID: env.ID}); err != nil { "UPDATE preview_environments SET status = 'expired', updated_at = NOW() WHERE id = $1",
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(ctx, db.(*database.DB), engine, *env.PreviewServiceID) removeServiceRuntime(c.Request.Context(), db.(*database.DB), engine, *env.PreviewServiceID)
} }
} }
_ = q.DeleteServiceByID(ctx, *env.PreviewServiceID) _, _ = db.(*database.DB).Exec(`DELETE FROM services WHERE id = $1`, *env.PreviewServiceID)
} }
cleanupCount++ cleanupCount++
} }
@@ -743,13 +754,15 @@ func resolvePreviewHost(db *database.DB, cloneID uuid.UUID, envName string) stri
if label == "" { if label == "" {
return "" return ""
} }
q := sqlcdb.New(db.DB) var nodeID sql.NullString
ctx := context.Background() var spread bool
brief, err := q.GetServicePlacementBrief(ctx, cloneID) if err := db.QueryRow(
if err != nil { `SELECT node_id, COALESCE(spread, false) FROM services WHERE id = $1`, cloneID,
).Scan(&nodeID, &spread); err != nil {
return "" return ""
} }
nodeID, spread := brief.NodeID, brief.Spread q := sqlcdb.New(db.DB)
ctx := context.Background()
var base string var base string
switch { switch {
case nodeID.Valid && nodeID.String != "": case nodeID.Valid && nodeID.String != "":
@@ -775,27 +788,31 @@ 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) {
r, err := sqlcdb.New(db.DB).GetServiceForDeploy(context.Background(), serviceID) var service Service
if err != nil { err := db.QueryRow(
return Service{}, err `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,
return serviceForDeployFromRow(r), nil 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,
)
return service, err
} }
// StartPreviewSweeper expires preview environments past their TTL — tears // StartPreviewSweeper expires preview environments past their TTL — tears
@@ -821,27 +838,54 @@ 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) {
q := sqlcdb.New(db.DB) rows, err := db.QueryContext(ctx,
rows, err := q.ListSweepablePreviews(ctx) `SELECT id, preview_service_id FROM preview_environments
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
for _, r := range rows { var clones []uuid.UUID
ids = append(ids, r.ID) for rows.Next() {
if r.PreviewServiceID.Valid { var id uuid.UUID
removeServiceRuntime(ctx, db, engine, r.PreviewServiceID.UUID) var cloneID *uuid.UUID
_ = q.DeleteServiceByID(ctx, r.PreviewServiceID.UUID) if err := rows.Scan(&id, &cloneID); err == nil {
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 {
_ = q.MarkPreviewEnvironmentStatus(ctx, sqlcdb.MarkPreviewEnvironmentStatusParams{ _, _ = db.ExecContext(ctx,
Status: "expired", ID: id}) `UPDATE preview_environments SET status = 'expired', updated_at = NOW() WHERE id = $1`, 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) {
_ = sqlcdb.New(db.DB).SyncPreviewStatuses(ctx) _, _ = db.ExecContext(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
`)
} }
+197 -103
View File
@@ -2,7 +2,6 @@ package api
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
@@ -11,7 +10,6 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
"containr/internal/secrets" "containr/internal/secrets"
@@ -24,17 +22,22 @@ import (
// service — covers pinned placements, spread replicas, and leftovers from a // service — covers pinned placements, spread replicas, and leftovers from a
// since-cleared pin. // since-cleared pin.
func remoteAgentsForService(ctx context.Context, db *database.DB, serviceID uuid.UUID) []string { func remoteAgentsForService(ctx context.Context, db *database.DB, serviceID uuid.UUID) []string {
ids, err := sqlcdb.New(db.DB).ListNodeAgentsForService(ctx, serviceID.String()) rows, err := db.Query(
`SELECT DISTINCT node_agent_id FROM container_instances WHERE service_id = $1`,
serviceID,
)
if err != nil { if err != nil {
return nil return nil
} }
out := make([]string, 0, len(ids)) defer rows.Close()
for _, id := range ids { var ids []string
if id != "" { for rows.Next() {
out = append(out, id) var id string
if rows.Scan(&id) == nil && id != "" {
ids = append(ids, id)
} }
} }
return out return ids
} }
// serviceLifecycle routes a start/stop/restart action to every agent holding // serviceLifecycle routes a start/stop/restart action to every agent holding
@@ -96,9 +99,7 @@ func serviceRuntimeSpec(db *database.DB, service Service) (deployment.RuntimeSpe
// Best effort: reuse the host port from the last live deployment so the // Best effort: reuse the host port from the last live deployment so the
// public URL survives restarts. Column exists post-migration; older DBs // public URL survives restarts. Column exists post-migration; older DBs
// silently get ephemeral ports. // silently get ephemeral ports.
if p, err := sqlcdb.New(db.DB).GetServicePublishedPort(context.Background(), service.ID); err == nil { _ = db.QueryRow(`SELECT published_port FROM services WHERE id = $1`, service.ID).Scan(&spec.PublishedPort)
spec.PublishedPort = p
}
if cmd := strings.TrimSpace(service.Command); cmd != "" { if cmd := strings.TrimSpace(service.Command); cmd != "" {
spec.Command = strings.Fields(cmd) spec.Command = strings.Fields(cmd)
} }
@@ -136,23 +137,42 @@ var refVarPattern = regexp.MustCompile(`\$\{\{\s*([A-Za-z0-9_-]+)\.([A-Za-z_][A-
// secrets) and expands ${{service.KEY}} references. KEY may be a variable on // secrets) and expands ${{service.KEY}} references. KEY may be a variable on
// the referenced service, or the builtins HOST (service name alias) and PORT. // the referenced service, or the builtins HOST (service name alias) and PORT.
func resolveServiceEnv(db *database.DB, service Service) (map[string]string, error) { func resolveServiceEnv(db *database.DB, service Service) (map[string]string, error) {
q := sqlcdb.New(db.DB) rows, err := db.Query(
vars, err := q.ListServiceVariableValues(context.Background(), service.ID) `SELECT key, value FROM environment_variables WHERE service_id = $1`,
service.ID,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
env := map[string]string{} env := map[string]string{}
for _, v := range vars { for rows.Next() {
env[v.Key] = secrets.Decrypt(v.Value) var k, v string
if err := rows.Scan(&k, &v); err != nil {
return nil, err
}
env[k] = secrets.Decrypt(v)
} }
refServices, err := q.ListProjectServiceRefs(context.Background(), service.ProjectID) var refServices []struct{ ID, Name string }
svcRows, err := db.Query(
`SELECT id, name FROM services WHERE project_id = $1`,
service.ProjectID,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer svcRows.Close()
for svcRows.Next() {
var s struct{ ID, Name string }
if err := svcRows.Scan(&s.ID, &s.Name); err != nil {
return nil, err
}
refServices = append(refServices, s)
}
byName := map[string]uuid.UUID{} byName := map[string]string{}
for _, s := range refServices { for _, s := range refServices {
byName[strings.ToLower(s.Name)] = s.ID byName[strings.ToLower(s.Name)] = s.ID
} }
@@ -169,15 +189,17 @@ func resolveServiceEnv(db *database.DB, service Service) (map[string]string, err
case "HOST": case "HOST":
return serviceName, true return serviceName, true
case "PORT": case "PORT":
if port, err := q.GetServicePort(context.Background(), id); err == nil && port > 0 { var port int
return strconv.Itoa(int(port)), true if err := db.QueryRow(`SELECT COALESCE(port, 0) FROM services WHERE id = $1`, id).Scan(&port); err == nil && port > 0 {
return strconv.Itoa(port), true
} }
return "", false return "", false
} }
value, err := q.GetServiceVariableValue(context.Background(), sqlcdb.GetServiceVariableValueParams{ var value string
ServiceID: id, err := db.QueryRow(
Key: key, `SELECT value FROM environment_variables WHERE service_id = $1 AND key = $2`,
}) id, key,
).Scan(&value)
if err != nil { if err != nil {
return "", false return "", false
} }
@@ -250,15 +272,6 @@ func parseMemoryLimit(mem string) int64 {
return 0 return 0
} }
// setServiceStatusNow writes the stored status with a fresh updated_at.
func setServiceStatusNow(db *database.DB, id uuid.UUID, status string) {
_ = sqlcdb.New(db.DB).SetServiceStatus(context.Background(), sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: status, Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: id,
})
}
func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.DB, bool) { func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.DB, bool) {
dbValue, exists := c.Get("db") dbValue, exists := c.Get("db")
if !exists { if !exists {
@@ -273,22 +286,6 @@ func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.D
return engineValue.(*deployment.DeploymentEngine), dbValue.(*database.DB), true return engineValue.(*deployment.DeploymentEngine), dbValue.(*database.DB), true
} }
// serviceFromRuntimeRow maps the sqlc runtime row (with legacy-column
// fallbacks applied in SQL) onto the API Service model.
func serviceFromRuntimeRow(r sqlcdb.GetServiceRuntimeWithOwnerRow) Service {
return Service{
ID: r.ID, ProjectID: r.ProjectID, Name: r.Name, Type: r.Type,
Status: r.Status, Image: r.Image, Command: r.Command,
Environment: r.Environment, GitRepo: r.GitRepo, GitBranch: r.GitBranch,
BuildPath: r.BuildPath, CPU: r.Cpu, Memory: r.Memory,
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 loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) { func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) {
serviceID, err := uuid.Parse(c.Param("id")) serviceID, err := uuid.Parse(c.Param("id"))
if err != nil { if err != nil {
@@ -301,16 +298,43 @@ func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) {
return Service{}, false return Service{}, false
} }
row, err := sqlcdb.New(db.DB).GetServiceRuntimeWithOwner(context.Background(), serviceID) var service Service
var owner string
err = db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(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, &owner,
)
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 Service{}, false return Service{}, false
} }
if row.OwnerID.String() != userID.(string) && !isAdminUser(db, userID.(string)) { if owner != userID.(string) && !isAdminUser(db, userID.(string)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return Service{}, false return Service{}, false
} }
return serviceFromRuntimeRow(row), true return service, true
} }
// loadReadableService loads a service when the caller may read its project: // loadReadableService loads a service when the caller may read its project:
@@ -322,13 +346,39 @@ func loadReadableService(c *gin.Context, db *database.DB) (Service, bool) {
return Service{}, false return Service{}, false
} }
row, err := sqlcdb.New(db.DB).GetServiceRuntimeWithOwner(context.Background(), serviceID) var service Service
var projectID uuid.UUID
err = db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(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, s.project_id
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, &projectID,
)
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 Service{}, false return Service{}, false
} }
service := serviceFromRuntimeRow(row) if _, allowed := projectReadAccess(c, db, projectID); !allowed {
if _, allowed := projectReadAccess(c, db, service.ProjectID); !allowed {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false return Service{}, false
} }
@@ -379,7 +429,7 @@ func reconcileNow(c *gin.Context, engine *deployment.DeploymentEngine, db *datab
status = state.Status status = state.Status
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
} }
setServiceStatusNow(db, service.ID, status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, status, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"runtime": state}) c.JSON(http.StatusOK, gin.H{"runtime": state})
return true return true
} }
@@ -398,7 +448,7 @@ func handleServiceStart(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "running") _, _ = db.Exec(`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "running"}) c.JSON(http.StatusOK, gin.H{"status": "running"})
} }
@@ -416,7 +466,7 @@ func handleServiceStop(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "stopped") _, _ = db.Exec(`UPDATE services SET status = 'stopped', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "stopped"}) c.JSON(http.StatusOK, gin.H{"status": "stopped"})
} }
@@ -435,7 +485,7 @@ func handleServiceRestart(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "running") _, _ = db.Exec(`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "running"}) c.JSON(http.StatusOK, gin.H{"status": "running"})
} }
@@ -455,8 +505,12 @@ func handleServiceRedeploy(c *gin.Context) {
image := service.Image image := service.Image
if image == "" { if image == "" {
// Git-sourced service: reuse the last successful deployment image. // Git-sourced service: reuse the last successful deployment image.
var err error err := db.QueryRow(
image, err = sqlcdb.New(db.DB).GetLastDeployedImage(context.Background(), service.ID) `SELECT image_name || ':' || image_tag FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`,
service.ID,
).Scan(&image)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No deployable image yet — run a deployment first"}) c.JSON(http.StatusBadRequest, gin.H{"error": "No deployable image yet — run a deployment first"})
return return
@@ -502,7 +556,7 @@ func reconcileServiceJob(ctx context.Context, db *database.DB, engine *deploymen
if state.Status != "" { if state.Status != "" {
status = state.Status status = state.Status
} }
setServiceStatusNow(db, service.ID, status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, status, time.Now(), service.ID)
} }
return state, nil return state, nil
} }
@@ -521,9 +575,12 @@ func enqueueServiceRedeploy(c *gin.Context, db *database.DB, service Service) bo
} }
image := service.Image image := service.Image
if image == "" { if image == "" {
var err error if err := db.QueryRow(
image, err = sqlcdb.New(db.DB).GetLastDeployedImage(context.Background(), service.ID) `SELECT image_name || ':' || image_tag FROM deployments
if err != nil { WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`,
service.ID,
).Scan(&image); err != nil {
return false return false
} }
} }
@@ -571,10 +628,10 @@ func persistPublishedPort(db *database.DB, serviceID uuid.UUID, ports []uint16)
if len(ports) == 0 { if len(ports) == 0 {
return return
} }
_ = sqlcdb.New(db.DB).SetServicePublishedPort(context.Background(), sqlcdb.SetServicePublishedPortParams{ _, _ = db.Exec(
PublishedPort: int32(ports[0]), `UPDATE services SET published_port = $1 WHERE id = $2 AND published_port <> $1`,
ID: serviceID, int(ports[0]), serviceID,
}) )
} }
// liveServiceStatus reconciles the stored status with real container state. // liveServiceStatus reconciles the stored status with real container state.
@@ -597,7 +654,7 @@ func liveServiceStatus(c *gin.Context, db *database.DB, service *Service) {
} }
if state.Status != service.Status { if state.Status != service.Status {
service.Status = state.Status service.Status = state.Status
setServiceStatusNow(db, service.ID, state.Status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, state.Status, time.Now(), service.ID)
} }
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
if service.Domain != "" { if service.Domain != "" {
@@ -619,22 +676,44 @@ func runtimeScaleService(c *gin.Context, serviceID string, replicas int) error {
return fmt.Errorf("runtime unavailable") return fmt.Errorf("runtime unavailable")
} }
q := sqlcdb.New(db.DB) var service Service
sid, err := uuid.Parse(serviceID) var owner string
err := db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(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, &owner,
)
if err != nil { if err != nil {
return err return err
} }
row, err := q.GetServiceRuntimeWithOwner(context.Background(), sid)
if err != nil {
return err
}
service := serviceFromRuntimeRow(row)
if service.Image == "" { if service.Image == "" {
image, err := q.GetLastDeployedImage(context.Background(), service.ID) if err := db.QueryRow(
if err != nil { `SELECT image_name || ':' || image_tag FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`, service.ID,
).Scan(&service.Image); err != nil {
return fmt.Errorf("no deployed image to scale") return fmt.Errorf("no deployed image to scale")
} }
service.Image = image
} }
spec, err := serviceRuntimeSpec(db, service) spec, err := serviceRuntimeSpec(db, service)
@@ -652,11 +731,7 @@ func runtimeScaleService(c *gin.Context, serviceID string, replicas int) error {
if state != nil { if state != nil {
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
} }
_ = q.SetServiceReplicas(context.Background(), sqlcdb.SetServiceReplicasParams{ _, _ = db.Exec(`UPDATE services SET replicas = $1, updated_at = $2 WHERE id = $3`, replicas, time.Now(), service.ID)
Replicas: int32(replicas),
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: service.ID,
})
return nil return nil
} }
@@ -675,34 +750,47 @@ func handleServiceEnvCheck(c *gin.Context) {
return return
} }
q := sqlcdb.New(db.DB) rows, err := db.Query(
vars, err := q.ListServiceVariablesWithSecret(context.Background(), service.ID) `SELECT key, value, COALESCE(is_secret, false) FROM environment_variables WHERE service_id = $1`,
service.ID,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
defer rows.Close()
var unresolved, empty, unreadable []string var unresolved, empty, unreadable []string
var raw []struct{ key, val string } var raw []struct{ key, val string }
for _, v := range vars { for rows.Next() {
decrypted := secrets.Decrypt(v.Value) var k, v string
if v.IsSecret && secrets.IsEncrypted(v.Value) && decrypted == v.Value { var secret bool
if err := rows.Scan(&k, &v, &secret); err != nil {
continue
}
decrypted := secrets.Decrypt(v)
if secret && secrets.IsEncrypted(v) && decrypted == v {
// Ciphertext failed to decrypt — Decrypt returns the input. // Ciphertext failed to decrypt — Decrypt returns the input.
unreadable = append(unreadable, v.Key) unreadable = append(unreadable, k)
continue continue
} }
if strings.TrimSpace(decrypted) == "" { if strings.TrimSpace(decrypted) == "" {
empty = append(empty, v.Key) empty = append(empty, k)
} }
raw = append(raw, struct{ key, val string }{v.Key, decrypted}) raw = append(raw, struct{ key, val string }{k, decrypted})
} }
// Project siblings for ${{name.KEY}} resolution. // Project siblings for ${{name.KEY}} resolution.
siblingIDs := map[string]uuid.UUID{} siblingIDs := map[string]string{}
if refs, err := q.ListProjectServiceRefs(context.Background(), service.ProjectID); err == nil { svcRows, err := db.Query(`SELECT id, name FROM services WHERE project_id = $1`, service.ProjectID)
for _, s := range refs { if err == nil {
siblingIDs[strings.ToLower(s.Name)] = s.ID for svcRows.Next() {
var id, name string
if err := svcRows.Scan(&id, &name); err == nil {
siblingIDs[strings.ToLower(name)] = id
}
} }
svcRows.Close()
} }
resolvable := func(serviceName, key string) bool { resolvable := func(serviceName, key string) bool {
@@ -714,14 +802,20 @@ func handleServiceEnvCheck(c *gin.Context) {
return true return true
} }
if strings.EqualFold(key, "PORT") { if strings.EqualFold(key, "PORT") {
port, err := q.GetServicePort(context.Background(), id) var port int
return err == nil && port > 0 if err := db.QueryRow(`SELECT COALESCE(port, 0) FROM services WHERE id = $1`, id).Scan(&port); err == nil {
return port > 0
}
return false
} }
_, err := q.GetServiceVariableValue(context.Background(), sqlcdb.GetServiceVariableValueParams{ var n int
ServiceID: id, if err := db.QueryRow(
Key: key, `SELECT COUNT(*) FROM environment_variables WHERE service_id = $1 AND key = $2`,
}) id, key,
return err == nil ).Scan(&n); err == nil {
return n > 0
}
return false
} }
for _, kv := range raw { for _, kv := range raw {
+109 -70
View File
@@ -2,9 +2,7 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/security" "containr/internal/security"
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -35,8 +33,8 @@ func NewSecurityHandler(db *database.DB, encryptionKey string) *SecurityHandler
// Notify the project owner when a background scan completes — criticals // Notify the project owner when a background scan completes — criticals
// earn a distinct title so they stand out in the notification list. // earn a distinct title so they stand out in the notification list.
scanner.OnScanComplete = func(scan *security.SecurityScan) { scanner.OnScanComplete = func(scan *security.SecurityScan) {
ownerID, err := sqlcdb.New(db.DB).GetProjectOwnerText(context.Background(), uuid.MustParse(scan.ProjectID)) var ownerID string
if err != nil || ownerID == "" { if err := db.QueryRow(`SELECT owner_id::text FROM projects WHERE id = $1`, scan.ProjectID).Scan(&ownerID); err != nil || ownerID == "" {
return return
} }
title := "Security scan completed" title := "Security scan completed"
@@ -83,10 +81,14 @@ func (sh *SecurityHandler) StartSecurityScan(c *gin.Context) {
return return
} }
serviceExists, err := sqlcdb.New(sh.db.DB).ServiceExistsInProject(context.Background(), sqlcdb.ServiceExistsInProjectParams{ var serviceExists bool
ID: uuid.MustParse(req.ServiceID), err := sh.db.QueryRow(
ProjectID: uuid.MustParse(req.ProjectID), `SELECT EXISTS(
}) SELECT 1 FROM services WHERE id = $1 AND project_id = $2
)`,
req.ServiceID,
req.ProjectID,
).Scan(&serviceExists)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate service"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate service"})
return return
@@ -162,30 +164,40 @@ func (sh *SecurityHandler) GetVulnerabilities(c *gin.Context) {
} }
// Query vulnerabilities // Query vulnerabilities
rows, err := sqlcdb.New(sh.db.DB).ListVulnerabilities(context.Background(), uuid.MustParse(projectID)) rows, err := sh.db.Query(`
SELECT id, type, severity, title, description, service_id, status, found_at, resolved_at
FROM vulnerabilities
WHERE project_id = $1
ORDER BY
CASE severity
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
found_at DESC
`, projectID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerabilities"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerabilities"})
return return
} }
defer rows.Close()
var vulnerabilities []security.Vulnerability var vulnerabilities []security.Vulnerability
for _, row := range rows { for rows.Next() {
// NULL description/service_id failed the original scan and dropped var vuln security.Vulnerability
// the row — keep the same filter. var resolvedAt *time.Time
if !row.Description.Valid || !row.ServiceID.Valid {
err := rows.Scan(&vuln.ID, &vuln.Type, &vuln.Severity, &vuln.Title, &vuln.Description,
&vuln.ServiceID, &vuln.Status, &vuln.FoundAt, &resolvedAt)
if err != nil {
continue continue
} }
vulnerabilities = append(vulnerabilities, security.Vulnerability{
ID: row.ID.String(), vuln.ResolvedAt = resolvedAt
Type: row.Type, vulnerabilities = append(vulnerabilities, vuln)
Severity: row.Severity,
Title: row.Title,
Description: row.Description.String,
ServiceID: row.ServiceID.UUID.String(),
Status: row.Status,
FoundAt: row.FoundAt,
ResolvedAt: timePtrNT(row.ResolvedAt),
})
} }
c.JSON(http.StatusOK, gin.H{"vulnerabilities": vulnerabilities}) c.JSON(http.StatusOK, gin.H{"vulnerabilities": vulnerabilities})
@@ -215,11 +227,11 @@ func (sh *SecurityHandler) UpdateVulnerability(c *gin.Context) {
resolvedAt = &now resolvedAt = &now
} }
err := sqlcdb.New(sh.db.DB).UpdateVulnerabilityStatus(context.Background(), sqlcdb.UpdateVulnerabilityStatusParams{ _, err := sh.db.Exec(`
Status: req.Status, UPDATE vulnerabilities
ResolvedAt: ntPtr(resolvedAt), SET status = $1, resolved_at = $2
ID: uuid.MustParse(vulnID), WHERE id = $3
}) `, req.Status, resolvedAt, vulnID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vulnerability"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vulnerability"})
@@ -259,7 +271,13 @@ func (sh *SecurityHandler) StartComplianceAssessment(c *gin.Context) {
return return
} }
frameworkExists, err := sqlcdb.New(sh.db.DB).ComplianceFrameworkExists(context.Background(), uuid.MustParse(req.FrameworkID)) var frameworkExists bool
err := sh.db.QueryRow(
`SELECT EXISTS(
SELECT 1 FROM compliance_frameworks WHERE id = $1
)`,
req.FrameworkID,
).Scan(&frameworkExists)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate framework"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate framework"})
return return
@@ -303,25 +321,30 @@ func (sh *SecurityHandler) GetComplianceReport(c *gin.Context) {
// GetComplianceFrameworks retrieves available compliance frameworks // GetComplianceFrameworks retrieves available compliance frameworks
func (sh *SecurityHandler) GetComplianceFrameworks(c *gin.Context) { func (sh *SecurityHandler) GetComplianceFrameworks(c *gin.Context) {
rows, err := sqlcdb.New(sh.db.DB).ListComplianceFrameworks(context.Background()) rows, err := sh.db.Query(`
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name
`)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance frameworks"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance frameworks"})
return return
} }
defer rows.Close()
var frameworks []security.ComplianceFramework var frameworks []security.ComplianceFramework
for _, row := range rows { for rows.Next() {
if !row.Description.Valid { var framework security.ComplianceFramework
err := rows.Scan(&framework.ID, &framework.Name, &framework.Description,
&framework.Version, &framework.Enabled, &framework.CreatedAt)
if err != nil {
continue continue
} }
frameworks = append(frameworks, security.ComplianceFramework{
ID: row.ID.String(), frameworks = append(frameworks, framework)
Name: row.Name,
Description: row.Description.String,
Version: row.Version,
Enabled: row.Enabled,
CreatedAt: row.CreatedAt,
})
} }
c.JSON(http.StatusOK, gin.H{"frameworks": frameworks}) c.JSON(http.StatusOK, gin.H{"frameworks": frameworks})
@@ -362,19 +385,23 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
Resolved int `json:"resolved"` Resolved int `json:"resolved"`
} }
q := sqlcdb.New(sh.db.DB) err := sh.db.QueryRow(`
mrow, err := q.GetVulnerabilityMetrics(context.Background(), uuid.MustParse(projectID)) SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE severity = 'critical') as critical,
COUNT(*) FILTER (WHERE severity = 'high') as high,
COUNT(*) FILTER (WHERE severity = 'medium') as medium,
COUNT(*) FILTER (WHERE severity = 'low') as low,
COUNT(*) FILTER (WHERE status = 'open') as open,
COUNT(*) FILTER (WHERE status = 'resolved') as resolved
FROM vulnerabilities
WHERE project_id = $1
`, projectID).Scan(&vulnMetrics.Total, &vulnMetrics.Critical, &vulnMetrics.High,
&vulnMetrics.Medium, &vulnMetrics.Low, &vulnMetrics.Open, &vulnMetrics.Resolved)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerability metrics"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerability metrics"})
return return
} }
vulnMetrics.Total = int(mrow.Total)
vulnMetrics.Critical = int(mrow.Critical)
vulnMetrics.High = int(mrow.High)
vulnMetrics.Medium = int(mrow.Medium)
vulnMetrics.Low = int(mrow.Low)
vulnMetrics.Open = int(mrow.Open)
vulnMetrics.Resolved = int(mrow.Resolved)
// Get latest scan // Get latest scan
var latestScan struct { var latestScan struct {
@@ -384,11 +411,13 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
Status string `json:"status"` Status string `json:"status"`
} }
srow, err := q.GetLatestSecurityScan(context.Background(), uuid.MustParse(projectID)) err = sh.db.QueryRow(`
latestScan.ID = srow.ID.String() SELECT id, COALESCE((summary->>'score')::int, 0), started_at as scanned_at, status
latestScan.Score = int(srow.Score) FROM security_scans
latestScan.ScannedAt = srow.ScannedAt WHERE project_id = $1
latestScan.Status = srow.Status ORDER BY started_at DESC
LIMIT 1
`, projectID).Scan(&latestScan.ID, &latestScan.Score, &latestScan.ScannedAt, &latestScan.Status)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
latestScan = struct { latestScan = struct {
@@ -409,10 +438,13 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
LastAssessed *time.Time `json:"last_assessed"` LastAssessed *time.Time `json:"last_assessed"`
} }
crow, err := q.GetLatestComplianceReport(context.Background(), uuid.MustParse(projectID)) err = sh.db.QueryRow(`
complianceStatus.OverallStatus = crow.OverallStatus SELECT overall_status, score, assessment_date
complianceStatus.Score = int(crow.Score) FROM compliance_reports
complianceStatus.LastAssessed = &crow.AssessmentDate WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1
`, projectID).Scan(&complianceStatus.OverallStatus, &complianceStatus.Score, &complianceStatus.LastAssessed)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
complianceStatus = struct { complianceStatus = struct {
@@ -582,11 +614,21 @@ func (sh *SecurityHandler) requireProjectAccess(c *gin.Context, projectID string
return "", false return "", false
} }
hasAccess, err := sqlcdb.New(sh.db.DB).CheckSecurityProjectAccess(context.Background(), sqlcdb.CheckSecurityProjectAccessParams{ var hasAccess bool
ID: uuid.MustParse(projectID), err := sh.db.QueryRow(
OwnerID: optionalUserUUID(c), `SELECT EXISTS (
Column3: contextIsAdmin(c), SELECT 1
}) FROM projects p
WHERE p.id = $1
AND (p.is_approved
OR $3::bool
OR p.owner_id = $2 OR EXISTS (
SELECT 1 FROM project_members pm
WHERE pm.project_id = p.id AND pm.user_id = $2
))
)`,
projectID, optionalUserUUID(c), contextIsAdmin(c),
).Scan(&hasAccess)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify project access"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify project access"})
return "", false return "", false
@@ -607,8 +649,7 @@ func (sh *SecurityHandler) requireSecurityScanAccess(c *gin.Context, scanID stri
} }
var projectID string var projectID string
pid, err := sqlcdb.New(sh.db.DB).GetSecurityScanProjectID(context.Background(), uuid.MustParse(scanID)) err := sh.db.QueryRow("SELECT project_id FROM security_scans WHERE id = $1", scanID).Scan(&projectID)
projectID = pid.String()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"})
return false return false
@@ -629,8 +670,7 @@ func (sh *SecurityHandler) requireComplianceReportAccess(c *gin.Context, reportI
} }
var projectID string var projectID string
pid, err := sqlcdb.New(sh.db.DB).GetComplianceReportProjectID(context.Background(), uuid.MustParse(reportID)) err := sh.db.QueryRow("SELECT project_id FROM compliance_reports WHERE id = $1", reportID).Scan(&projectID)
projectID = pid.String()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"})
return false return false
@@ -651,8 +691,7 @@ func (sh *SecurityHandler) requireVulnerabilityAccess(c *gin.Context, vulnID str
} }
var projectID string var projectID string
pid, err := sqlcdb.New(sh.db.DB).GetVulnerabilityProjectID(context.Background(), uuid.MustParse(vulnID)) err := sh.db.QueryRow("SELECT project_id FROM vulnerabilities WHERE id = $1", vulnID).Scan(&projectID)
projectID = pid.String()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Vulnerability not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Vulnerability not found"})
return "", false return "", false
-212
View File
@@ -1,212 +0,0 @@
package api
import (
"context"
"database/sql"
"strings"
"testing"
"time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
)
// Live invariant test against real Postgres — set METRICS_TEST_DSN (or
// DATABASE_URL). Covers the generated auth/security queries: user upsert
// conflict path, project-access EXISTS predicate, vulnerability ordering and
// status update, latest-scan/report lookups, and the framework list.
func TestSecurityLiveInvariants(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()
otherID := uuid.New()
projectID := uuid.New()
envID := uuid.New()
serviceID := uuid.New()
scanID := uuid.New()
reportID := uuid.New()
frameworkID := uuid.New()
vulnCrit := uuid.New()
vulnLow := uuid.New()
suffix := strings.ReplaceAll(userID.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'), ($3, $4, 'x', 'other')`,
userID, "sec-"+suffix+"@test.local", otherID, "sec-"+suffix+"[email protected]")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, false)`,
projectID, "sec-"+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)
VALUES ($1, $2, $3, $4, 'web', 'image')`,
serviceID, "sec-"+suffix, projectID, envID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id IN ($1, $2)`, userID, otherID) }()
// Auth queries: upsert conflicts update rather than error, counts see rows.
_, err = q.InsertUserAdmin(ctx, sqlcdb.InsertUserAdminParams{
Email: "ins-" + suffix + "@test.local", PasswordHash: "x", Name: "ins", IsAdmin: false,
})
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE email = $1`, "ins-"+suffix+"@test.local") }()
urow, err := q.UpsertLocalUser(ctx, sqlcdb.UpsertLocalUserParams{
Email: "ins-" + suffix + "@test.local", PasswordHash: "y", Name: "renamed",
Column4: "http://img", IsAdmin: false,
})
must(err)
if urow.Name != "renamed" || urow.AvatarUrl != "http://img" {
t.Fatalf("upsert: %+v", urow)
}
byEmail, err := q.GetUserByEmailForAuth(ctx, "ins-"+suffix+"@test.local")
must(err)
if byEmail.ID != urow.ID || byEmail.PasswordHash != "x" {
t.Fatalf("by email: %+v", byEmail)
}
count, err := q.CountUsersByEmail(ctx, "ins-"+suffix+"@test.local")
must(err)
if count != 1 {
t.Fatalf("count by email: %d", count)
}
byID, err := q.GetUserByID(ctx, urow.ID)
must(err)
if byID.Email != "ins-"+suffix+"@test.local" {
t.Fatalf("by id: %+v", byID)
}
// Access predicate: unapproved project is owner/admin/member only.
ownerAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: userID, Column3: false,
})
must(err)
if !ownerAccess {
t.Fatal("owner should have access")
}
strangerAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: otherID, Column3: false,
})
must(err)
if strangerAccess {
t.Fatal("stranger should not have access")
}
adminAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: otherID, Column3: true,
})
must(err)
if !adminAccess {
t.Fatal("admin should have access")
}
ownerText, err := q.GetProjectOwnerText(ctx, projectID)
must(err)
if ownerText != userID.String() {
t.Fatalf("owner text: %q", ownerText)
}
exists, err := q.ServiceExistsInProject(ctx, sqlcdb.ServiceExistsInProjectParams{
ID: serviceID, ProjectID: projectID,
})
must(err)
if !exists {
t.Fatal("service should exist in project")
}
// Scan/report/vuln rows via raw inserts (fixtures), reads via sqlc.
_, err = db.Exec(`INSERT INTO security_scans (id, project_id, service_id, scan_type, status, summary, started_at)
VALUES ($1, $2, $3, 'dependency', 'completed', '{"score": 85}', NOW() - interval '1 hour')`,
scanID, projectID, serviceID)
must(err)
_, err = db.Exec(`INSERT INTO compliance_frameworks (id, name, description, version, enabled)
VALUES ($1, $2, 'desc', '1.0', true)`,
frameworkID, "fw-"+suffix)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM compliance_frameworks WHERE id = $1`, frameworkID) }()
_, err = db.Exec(`INSERT INTO compliance_reports (id, project_id, framework_id, overall_status, score, assessment_date)
VALUES ($1, $2, $3, 'compliant', 92, NOW() - interval '30 minutes')`,
reportID, projectID, frameworkID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM compliance_reports WHERE id = $1`, reportID) }()
_, err = db.Exec(`INSERT INTO vulnerabilities (id, type, severity, title, description, service_id, project_id, status, found_at)
VALUES ($1, 'dependency', 'low', 'low-v', 'd', $3, $4, 'open', NOW() - interval '2 hours'),
($2, 'dependency', 'critical', 'crit-v', 'd', $3, $4, 'open', NOW() - interval '1 hour')`,
vulnLow, vulnCrit, serviceID, projectID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM vulnerabilities WHERE project_id = $1`, projectID) }()
defer func() { _, _ = db.Exec(`DELETE FROM security_scans WHERE id = $1`, scanID) }()
// Severity ordering: critical first.
vulns, err := q.ListVulnerabilities(ctx, projectID)
must(err)
if len(vulns) != 2 || vulns[0].ID != vulnCrit {
t.Fatalf("vuln ordering: %+v", vulns)
}
metrics, err := q.GetVulnerabilityMetrics(ctx, projectID)
must(err)
if metrics.Total != 2 || metrics.Critical != 1 || metrics.Low != 1 || metrics.Open != 2 {
t.Fatalf("metrics: %+v", metrics)
}
must(q.UpdateVulnerabilityStatus(ctx, sqlcdb.UpdateVulnerabilityStatusParams{
Status: "resolved",
ResolvedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: vulnCrit,
}))
metrics, err = q.GetVulnerabilityMetrics(ctx, projectID)
must(err)
if metrics.Open != 1 || metrics.Resolved != 1 {
t.Fatalf("metrics after resolve: %+v", metrics)
}
// Latest scan/report lookups.
scan, err := q.GetLatestSecurityScan(ctx, projectID)
must(err)
if scan.ID != scanID || scan.Score != 85 || scan.Status != "completed" {
t.Fatalf("latest scan: %+v", scan)
}
report, err := q.GetLatestComplianceReport(ctx, projectID)
must(err)
if report.OverallStatus != "compliant" || report.Score != 92 {
t.Fatalf("latest report: %+v", report)
}
// Project-ID lookups used by the access guards.
scanPID, err := q.GetSecurityScanProjectID(ctx, scanID)
must(err)
reportPID, err := q.GetComplianceReportProjectID(ctx, reportID)
must(err)
vulnPID, err := q.GetVulnerabilityProjectID(ctx, vulnCrit)
must(err)
if scanPID != projectID || reportPID != projectID || vulnPID != projectID {
t.Fatalf("project id lookups: %v %v %v", scanPID, reportPID, vulnPID)
}
// Framework list only returns enabled rows.
frameworks, err := q.ListComplianceFrameworks(ctx)
must(err)
found := false
for _, f := range frameworks {
if f.ID == frameworkID && f.Enabled {
found = true
}
}
if !found {
t.Fatal("enabled framework missing from list")
}
frameworkExists, err := q.ComplianceFrameworkExists(ctx, frameworkID)
must(err)
if !frameworkExists {
t.Fatal("framework should exist")
}
}
+60 -42
View File
@@ -1,14 +1,11 @@
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"
@@ -56,8 +53,11 @@ func handleCloneService(c *gin.Context) {
if name == "" { if name == "" {
name = source.Name + "-copy" name = source.Name + "-copy"
} }
count, _ := sqlcdb.New(db.(*database.DB).DB).CountServicesByName( var count int
c.Request.Context(), sqlcdb.CountServicesByNameParams{ProjectID: targetProjectID, Name: name}) _ = db.(*database.DB).QueryRow(
`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,11 +84,8 @@ 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) {
ctx := context.Background() var sourceEnv string
q := sqlcdb.New(db.DB) if err := db.QueryRow(`SELECT COALESCE(environment, '') FROM services WHERE id = $1`, sourceID).Scan(&sourceEnv); err != nil {
sourceEnv, err := q.GetServiceEnvironment(ctx, sourceID)
if err != nil {
return uuid.Nil, err return uuid.Nil, err
} }
if environment == "" { if environment == "" {
@@ -105,31 +102,46 @@ 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 = q.CloneServiceRow(ctx, sqlcdb.CloneServiceRowParams{ _, err = db.Exec(
ID: newID, `INSERT INTO services
ProjectID: targetProjectID, (id, project_id, name, environment_id, service_type, source_type, source_url, image_name,
Name: name, build_command, start_command, type, status, image, command, environment,
EnvironmentID: environmentID, git_repo, git_branch, build_path, cpu, memory, replicas, port, domain,
Environment: sql.NullString{String: environment, Valid: true}, healthcheck_path, restart_policy, volumes, maintenance_mode, basic_auth_users,
MaintenanceMode: maintenance, builder, cpu_reserve, memory_reserve, static_build_cmd, static_dir,
BasicAuthUsers: basicAuth, created_at, updated_at)
CreatedAt: sql.NullTime{Time: now, Valid: true}, SELECT $1, $2, $3, $4, service_type, source_type, source_url, image_name,
ID_2: sourceID, build_command, start_command, type, 'stopped', image, command, $5,
}) 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 := q.CloneServiceDomains(ctx, sqlcdb.CloneServiceDomainsParams{ if _, err := db.Exec(
ServiceID: newID, ServiceID_2: sourceID}); err != nil { `INSERT INTO service_domains (service_id, domain, is_default, cert_type)
_ = q.DeleteServiceByID(ctx, newID) SELECT $1, domain, is_default, cert_type FROM service_domains WHERE service_id = $2`,
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 := q.CloneServiceVariables(ctx, sqlcdb.CloneServiceVariablesParams{ if _, err := db.Exec(
ServiceID: newID, ServiceID_2: sourceID}); err != nil { `INSERT INTO environment_variables (id, service_id, key, value, is_secret)
_ = q.DeleteServiceByID(ctx, newID) SELECT gen_random_uuid(), $1, key, value, is_secret FROM environment_variables WHERE service_id = $2`,
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
@@ -175,8 +187,11 @@ func handleMoveService(c *gin.Context) {
return return
} }
count, _ := sqlcdb.New(db.(*database.DB).DB).CountServicesByName( var count int
c.Request.Context(), sqlcdb.CountServicesByNameParams{ProjectID: req.ProjectID, Name: service.Name}) _ = db.(*database.DB).QueryRow(
`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
@@ -192,18 +207,15 @@ func handleMoveService(c *gin.Context) {
return return
} }
n, err := sqlcdb.New(db.(*database.DB).DB).MoveServiceProject( res, err := db.(*database.DB).Exec(
c.Request.Context(), sqlcdb.MoveServiceProjectParams{ `UPDATE services SET project_id = $1, environment_id = $2, updated_at = $3 WHERE id = $4`,
ProjectID: req.ProjectID, req.ProjectID, environmentID, time.Now(), serviceID,
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 == 0 { if n, _ := res.RowsAffected(); 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
} }
@@ -217,10 +229,16 @@ func ownsProject(db *database.DB, projectID uuid.UUID, userID string, isAdmin bo
if isAdmin { if isAdmin {
return true return true
} }
uid, err := uuid.Parse(userID) var owner string
if err != nil { err := db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, projectID).Scan(&owner)
return false return err == nil && owner == userID
}
// loadServiceVolumesJSON returns the raw stored mounts for a verbatim copy.
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("[]")
} }
owner, err := sqlcdb.New(db.DB).GetProjectOwner(context.Background(), projectID) return raw
return err == nil && owner == uid
} }
+46 -18
View File
@@ -86,17 +86,20 @@ type sleepCandidate struct {
} }
func sweepSleeping(ctx context.Context, db *database.DB, dc *docker.Client, engine *deployment.DeploymentEngine, apiPort int) { func sweepSleeping(ctx context.Context, db *database.DB, dc *docker.Client, engine *deployment.DeploymentEngine, apiPort int) {
sweepRows, err := sqlcdb.New(db.DB).ListSleepCandidates(ctx) rows, err := db.QueryContext(ctx,
`SELECT id, project_id, name, COALESCE(status,''), COALESCE(sleep_idle_minutes,15)
FROM services WHERE sleep_enabled AND COALESCE(status,'') IN ('running','deployed','degraded','starting')`)
if err != nil { if err != nil {
return return
} }
var candidates []sleepCandidate var candidates []sleepCandidate
for _, r := range sweepRows { for rows.Next() {
candidates = append(candidates, sleepCandidate{ var s sleepCandidate
ID: r.ID, ProjectID: r.ProjectID, Name: r.Name, if err := rows.Scan(&s.ID, &s.ProjectID, &s.Name, &s.Status, &s.IdleMinutes); err == nil {
Status: r.Status, IdleMinutes: int(r.SleepIdleMinutes), candidates = append(candidates, s)
}) }
} }
rows.Close()
q := sqlcdb.New(db.DB) q := sqlcdb.New(db.DB)
for _, svc := range candidates { for _, svc := range candidates {
@@ -184,10 +187,11 @@ func putToSleepRemote(ctx context.Context, db *database.DB, q *sqlcdb.Queries, s
tracker.mu.Lock() tracker.mu.Lock()
delete(tracker.activity, svc.ID) delete(tracker.activity, svc.ID)
tracker.mu.Unlock() tracker.mu.Unlock()
setServiceStatusNow(db, svc.ID, "sleeping") _, _ = db.Exec(`UPDATE services SET status = 'sleeping', updated_at = $1 WHERE id = $2`, time.Now(), svc.ID)
log.Printf("sleep: %s (%s) remote replicas removed after idle timeout", svc.Name, svc.ID) log.Printf("sleep: %s (%s) remote replicas removed after idle timeout", svc.Name, svc.ID)
domain, _ := q.GetServiceDomainText(ctx, svc.ID) var domain string
_ = db.QueryRow(`SELECT COALESCE(domain,'') FROM services WHERE id = $1`, svc.ID).Scan(&domain)
domains := serviceDomainNames(db, svc.ID, domain) domains := serviceDomainNames(db, svc.ID, domain)
if len(domains) > 0 { if len(domains) > 0 {
writeWakeIngress(svc.ID.String(), domains, apiPort) writeWakeIngress(svc.ID.String(), domains, apiPort)
@@ -246,10 +250,11 @@ func putToSleep(ctx context.Context, db *database.DB, dc *docker.Client, engine
tracker.mu.Lock() tracker.mu.Lock()
delete(tracker.activity, svc.ID) delete(tracker.activity, svc.ID)
tracker.mu.Unlock() tracker.mu.Unlock()
setServiceStatusNow(db, svc.ID, "sleeping") _, _ = db.Exec(`UPDATE services SET status = 'sleeping', updated_at = $1 WHERE id = $2`, time.Now(), svc.ID)
log.Printf("sleep: %s (%s) scaled to zero after idle timeout", svc.Name, svc.ID) log.Printf("sleep: %s (%s) scaled to zero after idle timeout", svc.Name, svc.ID)
domain, _ := sqlcdb.New(db.DB).GetServiceDomainText(ctx, svc.ID) var domain string
_ = db.QueryRow(`SELECT COALESCE(domain,'') FROM services WHERE id = $1`, svc.ID).Scan(&domain)
domains := serviceDomainNames(db, svc.ID, domain) domains := serviceDomainNames(db, svc.ID, domain)
if len(domains) == 0 { if len(domains) == 0 {
return return
@@ -439,9 +444,7 @@ func pruneWakeContainers(ctx context.Context, db *database.DB, dc *docker.Client
} }
} }
var status string var status string
if sid, err := uuid.Parse(svcID); err == nil { _ = db.QueryRow(`SELECT COALESCE(status,'') FROM services WHERE id = $1`, svcID).Scan(&status)
status, _ = sqlcdb.New(db.DB).GetServiceStatusText(ctx, sid)
}
if live > 0 || status == "stopped" || status == "" { if live > 0 || status == "stopped" || status == "" {
_ = dc.RemoveContainer(ctx, c.ID, true) _ = dc.RemoveContainer(ctx, c.ID, true)
} }
@@ -488,11 +491,34 @@ func triggerServiceWake(c *gin.Context) (int, gin.H) {
if err != nil { if err != nil {
return http.StatusBadRequest, gin.H{"error": "invalid service id"} return http.StatusBadRequest, gin.H{"error": "invalid service id"}
} }
row, err := sqlcdb.New(db.DB).GetServiceRuntimeWithOwner(context.Background(), id) var service Service
err = db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(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`, id,
).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,
)
if err != nil { if err != nil {
return http.StatusNotFound, gin.H{"error": "service not found"} return http.StatusNotFound, gin.H{"error": "service not found"}
} }
service := serviceFromRuntimeRow(row)
if service.Status == "running" || service.Status == "deployed" { if service.Status == "running" || service.Status == "deployed" {
return http.StatusOK, gin.H{"status": service.Status} return http.StatusOK, gin.H{"status": service.Status}
} }
@@ -502,13 +528,13 @@ func triggerServiceWake(c *gin.Context) (int, gin.H) {
spec, err := serviceRuntimeSpec(db, service) spec, err := serviceRuntimeSpec(db, service)
if err != nil { if err != nil {
log.Printf("wake: runtime spec for %s failed: %v", service.ID, err) log.Printf("wake: runtime spec for %s failed: %v", service.ID, err)
setServiceStatusNow(db, service.ID, "failed") _, _ = db.Exec(`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
return return
} }
state, err := engine.ReconcileService(ctx, spec) state, err := engine.ReconcileService(ctx, spec)
if err != nil { if err != nil {
log.Printf("wake: reconcile for %s failed: %v", service.ID, err) log.Printf("wake: reconcile for %s failed: %v", service.ID, err)
setServiceStatusNow(db, service.ID, "failed") _, _ = db.Exec(`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
return return
} }
status := "running" status := "running"
@@ -516,7 +542,7 @@ func triggerServiceWake(c *gin.Context) (int, gin.H) {
status = state.Status status = state.Status
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
} }
setServiceStatusNow(db, service.ID, status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, status, time.Now(), service.ID)
}() }()
return http.StatusAccepted, gin.H{"status": "waking"} return http.StatusAccepted, gin.H{"status": "waking"}
} }
@@ -537,6 +563,8 @@ func handleServiceSleep(c *gin.Context) {
if !ok { if !ok {
return return
} }
var port int
_ = db.QueryRow(`SELECT COALESCE(port,0) FROM services WHERE id = $1`, service.ID).Scan(&port)
svc := sleepCandidate{ svc := sleepCandidate{
ID: service.ID, ID: service.ID,
ProjectID: service.ProjectID, ProjectID: service.ProjectID,
@@ -1,233 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: auth.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const countUsers = `-- name: CountUsers :one
SELECT COUNT(*) FROM users
`
func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, countUsers)
var count int64
err := row.Scan(&count)
return count, err
}
const countUsersByEmail = `-- name: CountUsersByEmail :one
SELECT COUNT(*) FROM users WHERE email = $1
`
func (q *Queries) CountUsersByEmail(ctx context.Context, email string) (int64, error) {
row := q.db.QueryRowContext(ctx, countUsersByEmail, email)
var count int64
err := row.Scan(&count)
return count, err
}
const getUserByEmailForAuth = `-- name: GetUserByEmailForAuth :one
SELECT id, email, password_hash, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE email = $1
`
type GetUserByEmailForAuthRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) GetUserByEmailForAuth(ctx context.Context, email string) (GetUserByEmailForAuthRow, error) {
row := q.db.QueryRowContext(ctx, getUserByEmailForAuth, email)
var i GetUserByEmailForAuthRow
err := row.Scan(
&i.ID,
&i.Email,
&i.PasswordHash,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE id = $1
`
type GetUserByIDRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error) {
row := q.db.QueryRowContext(ctx, getUserByID, id)
var i GetUserByIDRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const insertUser = `-- name: InsertUser :one
INSERT INTO users (email, password_hash, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
`
type InsertUserParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
}
type InsertUserRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (InsertUserRow, error) {
row := q.db.QueryRowContext(ctx, insertUser, arg.Email, arg.PasswordHash, arg.Name)
var i InsertUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const insertUserAdmin = `-- name: InsertUserAdmin :one
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, $4)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
`
type InsertUserAdminParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
IsAdmin bool `json:"is_admin"`
}
type InsertUserAdminRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams) (InsertUserAdminRow, error) {
row := q.db.QueryRowContext(ctx, insertUserAdmin,
arg.Email,
arg.PasswordHash,
arg.Name,
arg.IsAdmin,
)
var i InsertUserAdminRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const updateUserProfile = `-- name: UpdateUserProfile :exec
UPDATE users
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
WHERE id = $3
`
type UpdateUserProfileParams struct {
Column1 string `json:"column_1"`
Column2 string `json:"column_2"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error {
_, err := q.db.ExecContext(ctx, updateUserProfile, arg.Column1, arg.Column2, arg.ID)
return err
}
const upsertLocalUser = `-- name: UpsertLocalUser :one
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
VALUES ($1, $2, $3, NULLIF($4::varchar, ''), $5)
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name,
avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = NOW()
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
`
type UpsertLocalUserParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
Column4 string `json:"column_4"`
IsAdmin bool `json:"is_admin"`
}
type UpsertLocalUserRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) UpsertLocalUser(ctx context.Context, arg UpsertLocalUserParams) (UpsertLocalUserRow, error) {
row := q.db.QueryRowContext(ctx, upsertLocalUser,
arg.Email,
arg.PasswordHash,
arg.Name,
arg.Column4,
arg.IsAdmin,
)
var i UpsertLocalUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
@@ -1,332 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: cron.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const deleteCronJob = `-- name: DeleteCronJob :exec
DELETE FROM cron_jobs WHERE id = $1
`
func (q *Queries) DeleteCronJob(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteCronJob, id)
return err
}
const finishCronExecution = `-- name: FinishCronExecution :exec
UPDATE cron_executions SET finished_at = $1, status = $2, output = $3, error = $4 WHERE id = $5
`
type FinishCronExecutionParams struct {
FinishedAt sql.NullTime `json:"finished_at"`
Status sql.NullString `json:"status"`
Output sql.NullString `json:"output"`
Error sql.NullString `json:"error"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) FinishCronExecution(ctx context.Context, arg FinishCronExecutionParams) error {
_, err := q.db.ExecContext(ctx, finishCronExecution,
arg.FinishedAt,
arg.Status,
arg.Output,
arg.Error,
arg.ID,
)
return err
}
const getCronJob = `-- name: GetCronJob :one
SELECT id, project_id, service_id, name, schedule, command, timezone,
enabled, last_run_at, next_run_at, last_status, last_output,
retention, created_at, updated_at
FROM cron_jobs WHERE id = $1
`
func (q *Queries) GetCronJob(ctx context.Context, id uuid.UUID) (CronJob, error) {
row := q.db.QueryRowContext(ctx, getCronJob, id)
var i CronJob
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.Name,
&i.Schedule,
&i.Command,
&i.Timezone,
&i.Enabled,
&i.LastRunAt,
&i.NextRunAt,
&i.LastStatus,
&i.LastOutput,
&i.Retention,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getCronJobForTrigger = `-- name: GetCronJobForTrigger :one
SELECT cj.service_id, cj.command, cj.schedule, cj.timezone, cj.retention, p.owner_id
FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1
`
type GetCronJobForTriggerRow struct {
ServiceID uuid.UUID `json:"service_id"`
Command string `json:"command"`
Schedule string `json:"schedule"`
Timezone sql.NullString `json:"timezone"`
Retention sql.NullInt32 `json:"retention"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetCronJobForTrigger(ctx context.Context, id uuid.UUID) (GetCronJobForTriggerRow, error) {
row := q.db.QueryRowContext(ctx, getCronJobForTrigger, id)
var i GetCronJobForTriggerRow
err := row.Scan(
&i.ServiceID,
&i.Command,
&i.Schedule,
&i.Timezone,
&i.Retention,
&i.OwnerID,
)
return i, err
}
const getCronJobOwner = `-- name: GetCronJobOwner :one
SELECT p.owner_id FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1
`
func (q *Queries) GetCronJobOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getCronJobOwner, id)
var owner_id uuid.UUID
err := row.Scan(&owner_id)
return owner_id, err
}
const getCronJobProjectID = `-- name: GetCronJobProjectID :one
SELECT project_id FROM cron_jobs WHERE id = $1
`
func (q *Queries) GetCronJobProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getCronJobProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getCronOwnerViaService = `-- name: GetCronOwnerViaService :one
SELECT p.owner_id FROM projects p
JOIN services s ON s.project_id = p.id
WHERE s.id = $1
`
func (q *Queries) GetCronOwnerViaService(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getCronOwnerViaService, id)
var owner_id uuid.UUID
err := row.Scan(&owner_id)
return owner_id, err
}
const insertCronExecution = `-- name: InsertCronExecution :exec
INSERT INTO cron_executions (id, cron_job_id, started_at, status)
VALUES ($1, $2, $3, $4)
`
type InsertCronExecutionParams struct {
ID uuid.UUID `json:"id"`
CronJobID uuid.UUID `json:"cron_job_id"`
StartedAt time.Time `json:"started_at"`
Status sql.NullString `json:"status"`
}
func (q *Queries) InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error {
_, err := q.db.ExecContext(ctx, insertCronExecution,
arg.ID,
arg.CronJobID,
arg.StartedAt,
arg.Status,
)
return err
}
const insertCronJob = `-- name: InsertCronJob :exec
INSERT INTO cron_jobs (id, project_id, service_id, name, schedule, command, timezone, enabled, next_run_at, retention, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`
type InsertCronJobParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
Name string `json:"name"`
Schedule string `json:"schedule"`
Command string `json:"command"`
Timezone sql.NullString `json:"timezone"`
Enabled sql.NullBool `json:"enabled"`
NextRunAt sql.NullTime `json:"next_run_at"`
Retention sql.NullInt32 `json:"retention"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) InsertCronJob(ctx context.Context, arg InsertCronJobParams) error {
_, err := q.db.ExecContext(ctx, insertCronJob,
arg.ID,
arg.ProjectID,
arg.ServiceID,
arg.Name,
arg.Schedule,
arg.Command,
arg.Timezone,
arg.Enabled,
arg.NextRunAt,
arg.Retention,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const listCronExecutions = `-- name: ListCronExecutions :many
SELECT id, cron_job_id, started_at, finished_at, status, output, error
FROM cron_executions
WHERE cron_job_id = $1
ORDER BY started_at DESC
LIMIT 100
`
func (q *Queries) ListCronExecutions(ctx context.Context, cronJobID uuid.UUID) ([]CronExecution, error) {
rows, err := q.db.QueryContext(ctx, listCronExecutions, cronJobID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []CronExecution{}
for rows.Next() {
var i CronExecution
if err := rows.Scan(
&i.ID,
&i.CronJobID,
&i.StartedAt,
&i.FinishedAt,
&i.Status,
&i.Output,
&i.Error,
); 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 listDueCronJobs = `-- name: ListDueCronJobs :many
SELECT id, project_id, service_id, name, schedule, command, timezone, enabled, retention
FROM cron_jobs
WHERE enabled = TRUE AND next_run_at IS NOT NULL AND next_run_at <= NOW()
`
type ListDueCronJobsRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
Name string `json:"name"`
Schedule string `json:"schedule"`
Command string `json:"command"`
Timezone sql.NullString `json:"timezone"`
Enabled sql.NullBool `json:"enabled"`
Retention sql.NullInt32 `json:"retention"`
}
func (q *Queries) ListDueCronJobs(ctx context.Context) ([]ListDueCronJobsRow, error) {
rows, err := q.db.QueryContext(ctx, listDueCronJobs)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListDueCronJobsRow{}
for rows.Next() {
var i ListDueCronJobsRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.Name,
&i.Schedule,
&i.Command,
&i.Timezone,
&i.Enabled,
&i.Retention,
); 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 trimCronExecutions = `-- name: TrimCronExecutions :exec
DELETE FROM cron_executions WHERE cron_executions.cron_job_id = $1 AND cron_executions.id NOT IN (
SELECT ce.id FROM cron_executions ce WHERE ce.cron_job_id = $1
ORDER BY ce.started_at DESC LIMIT $2)
`
type TrimCronExecutionsParams struct {
CronJobID uuid.UUID `json:"cron_job_id"`
Limit int32 `json:"limit"`
}
func (q *Queries) TrimCronExecutions(ctx context.Context, arg TrimCronExecutionsParams) error {
_, err := q.db.ExecContext(ctx, trimCronExecutions, arg.CronJobID, arg.Limit)
return err
}
const updateCronJobRun = `-- name: UpdateCronJobRun :exec
UPDATE cron_jobs SET last_run_at = $1, last_status = $2, last_output = $3, next_run_at = $4 WHERE id = $5
`
type UpdateCronJobRunParams struct {
LastRunAt sql.NullTime `json:"last_run_at"`
LastStatus sql.NullString `json:"last_status"`
LastOutput sql.NullString `json:"last_output"`
NextRunAt sql.NullTime `json:"next_run_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateCronJobRun(ctx context.Context, arg UpdateCronJobRunParams) error {
_, err := q.db.ExecContext(ctx, updateCronJobRun,
arg.LastRunAt,
arg.LastStatus,
arg.LastOutput,
arg.NextRunAt,
arg.ID,
)
return err
}
@@ -1,536 +0,0 @@
// 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
}
@@ -1,242 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: domains.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const clearServiceDomainDefault = `-- name: ClearServiceDomainDefault :exec
UPDATE service_domains SET is_default = false WHERE service_id = $1
`
func (q *Queries) ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, clearServiceDomainDefault, serviceID)
return err
}
const countServiceDomains = `-- name: CountServiceDomains :one
SELECT COUNT(*) FROM service_domains WHERE service_id = $1
`
func (q *Queries) CountServiceDomains(ctx context.Context, serviceID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, countServiceDomains, serviceID)
var count int64
err := row.Scan(&count)
return count, err
}
const createServiceDomain = `-- name: CreateServiceDomain :one
INSERT INTO service_domains (service_id, domain, is_default)
VALUES ($1, $2, $3)
RETURNING id, service_id, domain, is_default, cert_type, cert_status, created_at
`
type CreateServiceDomainParams struct {
ServiceID uuid.UUID `json:"service_id"`
Domain string `json:"domain"`
IsDefault bool `json:"is_default"`
}
type CreateServiceDomainRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
Domain string `json:"domain"`
IsDefault bool `json:"is_default"`
CertType string `json:"cert_type"`
CertStatus string `json:"cert_status"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) CreateServiceDomain(ctx context.Context, arg CreateServiceDomainParams) (CreateServiceDomainRow, error) {
row := q.db.QueryRowContext(ctx, createServiceDomain, arg.ServiceID, arg.Domain, arg.IsDefault)
var i CreateServiceDomainRow
err := row.Scan(
&i.ID,
&i.ServiceID,
&i.Domain,
&i.IsDefault,
&i.CertType,
&i.CertStatus,
&i.CreatedAt,
)
return i, err
}
const deleteServiceDomain = `-- name: DeleteServiceDomain :execrows
DELETE FROM service_domains WHERE id = $1 AND service_id = $2
`
type DeleteServiceDomainParams struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
}
func (q *Queries) DeleteServiceDomain(ctx context.Context, arg DeleteServiceDomainParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteServiceDomain, arg.ID, arg.ServiceID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getServiceAccess = `-- name: GetServiceAccess :one
SELECT maintenance_mode, basic_auth_users FROM services WHERE id = $1
`
type GetServiceAccessRow struct {
MaintenanceMode bool `json:"maintenance_mode"`
BasicAuthUsers string `json:"basic_auth_users"`
}
func (q *Queries) GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error) {
row := q.db.QueryRowContext(ctx, getServiceAccess, id)
var i GetServiceAccessRow
err := row.Scan(&i.MaintenanceMode, &i.BasicAuthUsers)
return i, err
}
const getServiceDomainDefault = `-- name: GetServiceDomainDefault :one
SELECT domain FROM service_domains WHERE service_id = $1 AND is_default
`
func (q *Queries) GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceDomainDefault, serviceID)
var domain string
err := row.Scan(&domain)
return domain, err
}
const getServiceDomainFirst = `-- name: GetServiceDomainFirst :one
SELECT domain FROM service_domains WHERE service_id = $1 ORDER BY created_at ASC LIMIT 1
`
func (q *Queries) GetServiceDomainFirst(ctx context.Context, serviceID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceDomainFirst, serviceID)
var domain string
err := row.Scan(&domain)
return domain, err
}
const getServicePlacementBrief = `-- name: GetServicePlacementBrief :one
SELECT name, node_id, COALESCE(spread, false) AS spread FROM services WHERE id = $1
`
type GetServicePlacementBriefRow struct {
Name string `json:"name"`
NodeID sql.NullString `json:"node_id"`
Spread bool `json:"spread"`
}
func (q *Queries) GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error) {
row := q.db.QueryRowContext(ctx, getServicePlacementBrief, id)
var i GetServicePlacementBriefRow
err := row.Scan(&i.Name, &i.NodeID, &i.Spread)
return i, err
}
const listServiceDomains = `-- name: ListServiceDomains :many
SELECT id, service_id, domain, is_default, cert_type, cert_status, last_checked_at, created_at
FROM service_domains
WHERE service_id = $1
ORDER BY is_default DESC, created_at ASC
`
func (q *Queries) ListServiceDomains(ctx context.Context, serviceID uuid.UUID) ([]ServiceDomain, error) {
rows, err := q.db.QueryContext(ctx, listServiceDomains, serviceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ServiceDomain{}
for rows.Next() {
var i ServiceDomain
if err := rows.Scan(
&i.ID,
&i.ServiceID,
&i.Domain,
&i.IsDefault,
&i.CertType,
&i.CertStatus,
&i.LastCheckedAt,
&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 serviceWriteAccess = `-- name: ServiceWriteAccess :one
SELECT EXISTS(
SELECT 1 FROM services s JOIN projects p ON s.project_id = p.id
WHERE s.id = $1 AND (p.owner_id = $2 OR $3::bool)
) AS allowed
`
type ServiceWriteAccessParams struct {
ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"`
Column3 bool `json:"column_3"`
}
func (q *Queries) ServiceWriteAccess(ctx context.Context, arg ServiceWriteAccessParams) (bool, error) {
row := q.db.QueryRowContext(ctx, serviceWriteAccess, arg.ID, arg.OwnerID, arg.Column3)
var allowed bool
err := row.Scan(&allowed)
return allowed, err
}
const setServiceDomainDefault = `-- name: SetServiceDomainDefault :execrows
UPDATE service_domains SET is_default = (id = $1) WHERE service_id = $2
`
type SetServiceDomainDefaultParams struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
}
func (q *Queries) SetServiceDomainDefault(ctx context.Context, arg SetServiceDomainDefaultParams) (int64, error) {
result, err := q.db.ExecContext(ctx, setServiceDomainDefault, arg.ID, arg.ServiceID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const setServiceLegacyDomain = `-- name: SetServiceLegacyDomain :exec
UPDATE services SET domain = $1 WHERE id = $2
`
type SetServiceLegacyDomainParams struct {
Domain string `json:"domain"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetServiceLegacyDomain(ctx context.Context, arg SetServiceLegacyDomainParams) error {
_, err := q.db.ExecContext(ctx, setServiceLegacyDomain, arg.Domain, arg.ID)
return err
}
const touchServiceDomainChecked = `-- name: TouchServiceDomainChecked :exec
UPDATE service_domains SET last_checked_at = NOW() WHERE id = $1
`
func (q *Queries) TouchServiceDomainChecked(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, touchServiceDomainChecked, id)
return err
}
@@ -1,737 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: git.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const countGitRepositories = `-- name: CountGitRepositories :one
SELECT COUNT(*)
FROM git_repositories
WHERE provider_id = $1 AND user_id = $2
AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'))
`
type CountGitRepositoriesParams struct {
ProviderID uuid.UUID `json:"provider_id"`
UserID uuid.UUID `json:"user_id"`
Column3 string `json:"column_3"`
}
func (q *Queries) CountGitRepositories(ctx context.Context, arg CountGitRepositoriesParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countGitRepositories, arg.ProviderID, arg.UserID, arg.Column3)
var count int64
err := row.Scan(&count)
return count, err
}
const countUserRepositories = `-- name: CountUserRepositories :one
SELECT COUNT(*) FROM git_repositories WHERE user_id = $1
`
func (q *Queries) CountUserRepositories(ctx context.Context, userID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, countUserRepositories, userID)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteGitProvider = `-- name: DeleteGitProvider :execrows
DELETE FROM git_providers WHERE id = $1 AND user_id = $2
`
type DeleteGitProviderParams struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) DeleteGitProvider(ctx context.Context, arg DeleteGitProviderParams) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteGitProvider, arg.ID, arg.UserID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getGitProviderForFetch = `-- name: GetGitProviderForFetch :one
SELECT name, api_url, access_token
FROM git_providers
WHERE id = $1 AND user_id = $2
`
type GetGitProviderForFetchParams struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"`
}
type GetGitProviderForFetchRow struct {
Name string `json:"name"`
ApiUrl string `json:"api_url"`
AccessToken string `json:"access_token"`
}
func (q *Queries) GetGitProviderForFetch(ctx context.Context, arg GetGitProviderForFetchParams) (GetGitProviderForFetchRow, error) {
row := q.db.QueryRowContext(ctx, getGitProviderForFetch, arg.ID, arg.UserID)
var i GetGitProviderForFetchRow
err := row.Scan(&i.Name, &i.ApiUrl, &i.AccessToken)
return i, err
}
const getGitProviderForRepos = `-- name: GetGitProviderForRepos :one
SELECT name, display_name, access_token, api_url
FROM git_providers
WHERE id = $1 AND user_id = $2
`
type GetGitProviderForReposParams struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"`
}
type GetGitProviderForReposRow struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
AccessToken string `json:"access_token"`
ApiUrl string `json:"api_url"`
}
func (q *Queries) GetGitProviderForRepos(ctx context.Context, arg GetGitProviderForReposParams) (GetGitProviderForReposRow, error) {
row := q.db.QueryRowContext(ctx, getGitProviderForRepos, arg.ID, arg.UserID)
var i GetGitProviderForReposRow
err := row.Scan(
&i.Name,
&i.DisplayName,
&i.AccessToken,
&i.ApiUrl,
)
return i, err
}
const getGitProviderName = `-- name: GetGitProviderName :one
SELECT name FROM git_providers WHERE id = $1 AND user_id = $2
`
type GetGitProviderNameParams struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) GetGitProviderName(ctx context.Context, arg GetGitProviderNameParams) (string, error) {
row := q.db.QueryRowContext(ctx, getGitProviderName, arg.ID, arg.UserID)
var name string
err := row.Scan(&name)
return name, err
}
const getGitProviderNameByID = `-- name: GetGitProviderNameByID :one
SELECT name FROM git_providers WHERE id = $1
`
func (q *Queries) GetGitProviderNameByID(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getGitProviderNameByID, id)
var name string
err := row.Scan(&name)
return name, err
}
const getGitProviderTimestamps = `-- name: GetGitProviderTimestamps :one
SELECT created_at, updated_at FROM git_providers WHERE id = $1
`
type GetGitProviderTimestampsRow struct {
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) GetGitProviderTimestamps(ctx context.Context, id uuid.UUID) (GetGitProviderTimestampsRow, error) {
row := q.db.QueryRowContext(ctx, getGitProviderTimestamps, id)
var i GetGitProviderTimestampsRow
err := row.Scan(&i.CreatedAt, &i.UpdatedAt)
return i, err
}
const getGitRepoForPush = `-- name: GetGitRepoForPush :one
SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1
`
type GetGitRepoForPushRow struct {
CloneUrl string `json:"clone_url"`
FullName string `json:"full_name"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) GetGitRepoForPush(ctx context.Context, id uuid.UUID) (GetGitRepoForPushRow, error) {
row := q.db.QueryRowContext(ctx, getGitRepoForPush, id)
var i GetGitRepoForPushRow
err := row.Scan(&i.CloneUrl, &i.FullName, &i.UserID)
return i, err
}
const getGitRepositoryByFullName = `-- name: GetGitRepositoryByFullName :one
SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description,
clone_url, COALESCE(webhook_url, '') AS webhook_url,
default_branch, is_private, user_id, created_at, updated_at
FROM git_repositories
WHERE provider_id = $1 AND full_name = $2 AND user_id = $3
`
type GetGitRepositoryByFullNameParams struct {
ProviderID uuid.UUID `json:"provider_id"`
FullName string `json:"full_name"`
UserID uuid.UUID `json:"user_id"`
}
type GetGitRepositoryByFullNameRow struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
CloneUrl string `json:"clone_url"`
WebhookUrl string `json:"webhook_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) GetGitRepositoryByFullName(ctx context.Context, arg GetGitRepositoryByFullNameParams) (GetGitRepositoryByFullNameRow, error) {
row := q.db.QueryRowContext(ctx, getGitRepositoryByFullName, arg.ProviderID, arg.FullName, arg.UserID)
var i GetGitRepositoryByFullNameRow
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.Name,
&i.FullName,
&i.Description,
&i.CloneUrl,
&i.WebhookUrl,
&i.DefaultBranch,
&i.IsPrivate,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getGitRepositoryProviderID = `-- name: GetGitRepositoryProviderID :one
SELECT provider_id FROM git_repositories WHERE id = $1 AND user_id = $2
`
type GetGitRepositoryProviderIDParams struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) GetGitRepositoryProviderID(ctx context.Context, arg GetGitRepositoryProviderIDParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getGitRepositoryProviderID, arg.ID, arg.UserID)
var provider_id uuid.UUID
err := row.Scan(&provider_id)
return provider_id, err
}
const getGitWebhookForPush = `-- name: GetGitWebhookForPush :one
SELECT webhook_secret, repo_id, provider_id, COALESCE(branch_filter, '') AS branch_filter, active
FROM git_webhooks WHERE id = $1
`
type GetGitWebhookForPushRow struct {
WebhookSecret string `json:"webhook_secret"`
RepoID uuid.UUID `json:"repo_id"`
ProviderID uuid.UUID `json:"provider_id"`
BranchFilter string `json:"branch_filter"`
Active sql.NullBool `json:"active"`
}
func (q *Queries) GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error) {
row := q.db.QueryRowContext(ctx, getGitWebhookForPush, id)
var i GetGitWebhookForPushRow
err := row.Scan(
&i.WebhookSecret,
&i.RepoID,
&i.ProviderID,
&i.BranchFilter,
&i.Active,
)
return i, err
}
const insertGitProvider = `-- name: InsertGitProvider :exec
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
`
type InsertGitProviderParams struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
ApiUrl string `json:"api_url"`
WebhookUrl string `json:"webhook_url"`
AccessToken string `json:"access_token"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) InsertGitProvider(ctx context.Context, arg InsertGitProviderParams) error {
_, err := q.db.ExecContext(ctx, insertGitProvider,
arg.ID,
arg.Name,
arg.DisplayName,
arg.ApiUrl,
arg.WebhookUrl,
arg.AccessToken,
arg.UserID,
)
return err
}
const insertGitRepository = `-- name: InsertGitRepository :one
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
RETURNING created_at, updated_at
`
type InsertGitRepositoryParams struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description sql.NullString `json:"description"`
CloneUrl string `json:"clone_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
}
type InsertGitRepositoryRow struct {
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) InsertGitRepository(ctx context.Context, arg InsertGitRepositoryParams) (InsertGitRepositoryRow, error) {
row := q.db.QueryRowContext(ctx, insertGitRepository,
arg.ID,
arg.ProviderID,
arg.Name,
arg.FullName,
arg.Description,
arg.CloneUrl,
arg.DefaultBranch,
arg.IsPrivate,
arg.UserID,
)
var i InsertGitRepositoryRow
err := row.Scan(&i.CreatedAt, &i.UpdatedAt)
return i, err
}
const listConnectedRepositories = `-- name: ListConnectedRepositories :many
SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.description, '') AS description, r.clone_url,
r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at,
p.name AS provider_name, p.display_name
FROM git_repositories r
JOIN git_providers p ON r.provider_id = p.id
WHERE r.user_id = $1
ORDER BY r.updated_at DESC
LIMIT $2 OFFSET $3
`
type ListConnectedRepositoriesParams struct {
UserID uuid.UUID `json:"user_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListConnectedRepositoriesRow struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
CloneUrl string `json:"clone_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
ProviderName string `json:"provider_name"`
DisplayName string `json:"display_name"`
}
func (q *Queries) ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error) {
rows, err := q.db.QueryContext(ctx, listConnectedRepositories, arg.UserID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListConnectedRepositoriesRow{}
for rows.Next() {
var i ListConnectedRepositoriesRow
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.Name,
&i.FullName,
&i.Description,
&i.CloneUrl,
&i.DefaultBranch,
&i.IsPrivate,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.ProviderName,
&i.DisplayName,
); 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 listGitProviders = `-- name: ListGitProviders :many
SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at
FROM git_providers
WHERE user_id = $1
ORDER BY created_at DESC
`
type ListGitProvidersRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
ApiUrl string `json:"api_url"`
WebhookUrl string `json:"webhook_url"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) ListGitProviders(ctx context.Context, userID uuid.UUID) ([]ListGitProvidersRow, error) {
rows, err := q.db.QueryContext(ctx, listGitProviders, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListGitProvidersRow{}
for rows.Next() {
var i ListGitProvidersRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.ApiUrl,
&i.WebhookUrl,
&i.UserID,
&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 listGitRepositories = `-- name: ListGitRepositories :many
SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description,
clone_url, COALESCE(webhook_url, '') AS webhook_url,
default_branch, is_private, user_id, created_at, updated_at
FROM git_repositories
WHERE provider_id = $1 AND user_id = $2
AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'))
ORDER BY updated_at DESC
LIMIT $4 OFFSET $5
`
type ListGitRepositoriesParams struct {
ProviderID uuid.UUID `json:"provider_id"`
UserID uuid.UUID `json:"user_id"`
Column3 string `json:"column_3"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListGitRepositoriesRow struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
CloneUrl string `json:"clone_url"`
WebhookUrl string `json:"webhook_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) ListGitRepositories(ctx context.Context, arg ListGitRepositoriesParams) ([]ListGitRepositoriesRow, error) {
rows, err := q.db.QueryContext(ctx, listGitRepositories,
arg.ProviderID,
arg.UserID,
arg.Column3,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListGitRepositoriesRow{}
for rows.Next() {
var i ListGitRepositoriesRow
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.Name,
&i.FullName,
&i.Description,
&i.CloneUrl,
&i.WebhookUrl,
&i.DefaultBranch,
&i.IsPrivate,
&i.UserID,
&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 listServicesForPush = `-- name: ListServicesForPush :many
SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status,
COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command,
COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo,
COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path,
COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS memory,
s.created_at, s.updated_at
FROM services s
WHERE (s.git_branch = $1 OR $1 = '') AND (s.git_repo = $2 OR s.git_repo = $3)
`
type ListServicesForPushParams struct {
GitBranch sql.NullString `json:"git_branch"`
GitRepo sql.NullString `json:"git_repo"`
GitRepo_2 sql.NullString `json:"git_repo_2"`
}
type ListServicesForPushRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
Image string `json:"image"`
Command string `json:"command"`
Environment string `json:"environment"`
GitRepo string `json:"git_repo"`
GitBranch string `json:"git_branch"`
BuildPath string `json:"build_path"`
Cpu string `json:"cpu"`
Memory string `json:"memory"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) ListServicesForPush(ctx context.Context, arg ListServicesForPushParams) ([]ListServicesForPushRow, error) {
rows, err := q.db.QueryContext(ctx, listServicesForPush, arg.GitBranch, arg.GitRepo, arg.GitRepo_2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListServicesForPushRow{}
for rows.Next() {
var i ListServicesForPushRow
if err := rows.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.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 upsertGitHubAppProvider = `-- name: UpsertGitHubAppProvider :one
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
ON CONFLICT (name, user_id)
DO UPDATE SET
display_name = EXCLUDED.display_name,
api_url = EXCLUDED.api_url,
webhook_url = EXCLUDED.webhook_url,
access_token = EXCLUDED.access_token,
updated_at = NOW()
RETURNING id, created_at, updated_at
`
type UpsertGitHubAppProviderParams struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
ApiUrl string `json:"api_url"`
WebhookUrl string `json:"webhook_url"`
AccessToken string `json:"access_token"`
UserID uuid.UUID `json:"user_id"`
}
type UpsertGitHubAppProviderRow struct {
ID uuid.UUID `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error) {
row := q.db.QueryRowContext(ctx, upsertGitHubAppProvider,
arg.ID,
arg.Name,
arg.DisplayName,
arg.ApiUrl,
arg.WebhookUrl,
arg.AccessToken,
arg.UserID,
)
var i UpsertGitHubAppProviderRow
err := row.Scan(&i.ID, &i.CreatedAt, &i.UpdatedAt)
return i, err
}
const upsertGitRepository = `-- name: UpsertGitRepository :exec
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
ON CONFLICT (provider_id, full_name)
DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
clone_url = EXCLUDED.clone_url,
default_branch = EXCLUDED.default_branch,
is_private = EXCLUDED.is_private,
updated_at = NOW()
`
type UpsertGitRepositoryParams struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description sql.NullString `json:"description"`
CloneUrl string `json:"clone_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error {
_, err := q.db.ExecContext(ctx, upsertGitRepository,
arg.ID,
arg.ProviderID,
arg.Name,
arg.FullName,
arg.Description,
arg.CloneUrl,
arg.DefaultBranch,
arg.IsPrivate,
arg.UserID,
)
return err
}
const upsertGitWebhook = `-- name: UpsertGitWebhook :one
INSERT INTO git_webhooks (id, repo_id, provider_id, events, webhook_secret, active, branch_filter, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, TRUE, $6, NOW(), NOW())
ON CONFLICT (repo_id, provider_id)
DO UPDATE SET
events = EXCLUDED.events,
webhook_secret = EXCLUDED.webhook_secret,
active = TRUE,
branch_filter = EXCLUDED.branch_filter,
updated_at = NOW()
RETURNING id, active, created_at, updated_at
`
type UpsertGitWebhookParams struct {
ID uuid.UUID `json:"id"`
RepoID uuid.UUID `json:"repo_id"`
ProviderID uuid.UUID `json:"provider_id"`
Events string `json:"events"`
WebhookSecret string `json:"webhook_secret"`
BranchFilter sql.NullString `json:"branch_filter"`
}
type UpsertGitWebhookRow struct {
ID uuid.UUID `json:"id"`
Active sql.NullBool `json:"active"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) UpsertGitWebhook(ctx context.Context, arg UpsertGitWebhookParams) (UpsertGitWebhookRow, error) {
row := q.db.QueryRowContext(ctx, upsertGitWebhook,
arg.ID,
arg.RepoID,
arg.ProviderID,
arg.Events,
arg.WebhookSecret,
arg.BranchFilter,
)
var i UpsertGitWebhookRow
err := row.Scan(
&i.ID,
&i.Active,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
@@ -1,623 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: metrics.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
)
const aggregateNodeMetrics = `-- name: AggregateNodeMetrics :many
SELECT date_bin($1::interval, timestamp, '2000-01-01'::timestamptz) AS bucket,
AVG(cpu_usage)::float8 AS avg_cpu,
AVG(memory_usage_percent)::float8 AS avg_memory,
AVG(storage_usage_percent)::float8 AS avg_storage
FROM node_metrics
WHERE node_id = $2 AND timestamp BETWEEN $3 AND $4
GROUP BY bucket
ORDER BY bucket ASC
`
type AggregateNodeMetricsParams struct {
Column1 int64 `json:"column_1"`
NodeID string `json:"node_id"`
Timestamp time.Time `json:"timestamp"`
Timestamp_2 time.Time `json:"timestamp_2"`
}
type AggregateNodeMetricsRow struct {
Bucket interface{} `json:"bucket"`
AvgCpu float64 `json:"avg_cpu"`
AvgMemory float64 `json:"avg_memory"`
AvgStorage float64 `json:"avg_storage"`
}
// Portable bucket bucketing (no TimescaleDB): floor epoch seconds into
// bucket-sized buckets, then back to timestamptz.
func (q *Queries) AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error) {
rows, err := q.db.QueryContext(ctx, aggregateNodeMetrics,
arg.Column1,
arg.NodeID,
arg.Timestamp,
arg.Timestamp_2,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []AggregateNodeMetricsRow{}
for rows.Next() {
var i AggregateNodeMetricsRow
if err := rows.Scan(
&i.Bucket,
&i.AvgCpu,
&i.AvgMemory,
&i.AvgStorage,
); 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 listInstanceMetrics = `-- name: ListInstanceMetrics :many
SELECT instance_id, node_id, status, cpu, memory,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
start_time, last_seen, health_status, health_last_check, health_check_count, health_failure_count
FROM instance_metrics
WHERE service_id = $1 AND timestamp = $2
`
type ListInstanceMetricsParams struct {
ServiceID string `json:"service_id"`
Timestamp time.Time `json:"timestamp"`
}
type ListInstanceMetricsRow struct {
InstanceID string `json:"instance_id"`
NodeID sql.NullString `json:"node_id"`
Status sql.NullString `json:"status"`
Cpu sql.NullString `json:"cpu"`
Memory sql.NullInt64 `json:"memory"`
NetworkBytesIn sql.NullInt64 `json:"network_bytes_in"`
NetworkBytesOut sql.NullInt64 `json:"network_bytes_out"`
NetworkPacketsIn sql.NullInt64 `json:"network_packets_in"`
NetworkPacketsOut sql.NullInt64 `json:"network_packets_out"`
NetworkConnectionsIn sql.NullInt32 `json:"network_connections_in"`
NetworkConnectionsOut sql.NullInt32 `json:"network_connections_out"`
NetworkErrorsIn sql.NullInt64 `json:"network_errors_in"`
NetworkErrorsOut sql.NullInt64 `json:"network_errors_out"`
StartTime sql.NullTime `json:"start_time"`
LastSeen sql.NullTime `json:"last_seen"`
HealthStatus sql.NullString `json:"health_status"`
HealthLastCheck sql.NullTime `json:"health_last_check"`
HealthCheckCount sql.NullInt32 `json:"health_check_count"`
HealthFailureCount sql.NullInt32 `json:"health_failure_count"`
}
func (q *Queries) ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error) {
rows, err := q.db.QueryContext(ctx, listInstanceMetrics, arg.ServiceID, arg.Timestamp)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListInstanceMetricsRow{}
for rows.Next() {
var i ListInstanceMetricsRow
if err := rows.Scan(
&i.InstanceID,
&i.NodeID,
&i.Status,
&i.Cpu,
&i.Memory,
&i.NetworkBytesIn,
&i.NetworkBytesOut,
&i.NetworkPacketsIn,
&i.NetworkPacketsOut,
&i.NetworkConnectionsIn,
&i.NetworkConnectionsOut,
&i.NetworkErrorsIn,
&i.NetworkErrorsOut,
&i.StartTime,
&i.LastSeen,
&i.HealthStatus,
&i.HealthLastCheck,
&i.HealthCheckCount,
&i.HealthFailureCount,
); 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 listNodeMetrics = `-- name: ListNodeMetrics :many
SELECT node_id, timestamp, cpu_usage, cpu_cores, load_avg_1, load_avg_5, load_avg_15,
memory_total, memory_used, memory_available, memory_usage_percent,
storage_total, storage_used, storage_available, storage_usage_percent,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
coalesce(extract(epoch from uptime), 0)::bigint AS uptime_seconds,
processes, os, kernel, architecture
FROM node_metrics
WHERE node_id = $1 AND timestamp BETWEEN $2 AND $3
ORDER BY timestamp ASC
`
type ListNodeMetricsParams struct {
NodeID string `json:"node_id"`
Timestamp time.Time `json:"timestamp"`
Timestamp_2 time.Time `json:"timestamp_2"`
}
type ListNodeMetricsRow struct {
NodeID string `json:"node_id"`
Timestamp time.Time `json:"timestamp"`
CpuUsage sql.NullString `json:"cpu_usage"`
CpuCores sql.NullString `json:"cpu_cores"`
LoadAvg1 sql.NullString `json:"load_avg_1"`
LoadAvg5 sql.NullString `json:"load_avg_5"`
LoadAvg15 sql.NullString `json:"load_avg_15"`
MemoryTotal sql.NullInt64 `json:"memory_total"`
MemoryUsed sql.NullInt64 `json:"memory_used"`
MemoryAvailable sql.NullInt64 `json:"memory_available"`
MemoryUsagePercent sql.NullString `json:"memory_usage_percent"`
StorageTotal sql.NullInt64 `json:"storage_total"`
StorageUsed sql.NullInt64 `json:"storage_used"`
StorageAvailable sql.NullInt64 `json:"storage_available"`
StorageUsagePercent sql.NullString `json:"storage_usage_percent"`
NetworkBytesIn sql.NullInt64 `json:"network_bytes_in"`
NetworkBytesOut sql.NullInt64 `json:"network_bytes_out"`
NetworkPacketsIn sql.NullInt64 `json:"network_packets_in"`
NetworkPacketsOut sql.NullInt64 `json:"network_packets_out"`
NetworkConnectionsIn sql.NullInt32 `json:"network_connections_in"`
NetworkConnectionsOut sql.NullInt32 `json:"network_connections_out"`
NetworkErrorsIn sql.NullInt64 `json:"network_errors_in"`
NetworkErrorsOut sql.NullInt64 `json:"network_errors_out"`
UptimeSeconds int64 `json:"uptime_seconds"`
Processes sql.NullInt32 `json:"processes"`
Os sql.NullString `json:"os"`
Kernel sql.NullString `json:"kernel"`
Architecture sql.NullString `json:"architecture"`
}
func (q *Queries) ListNodeMetrics(ctx context.Context, arg ListNodeMetricsParams) ([]ListNodeMetricsRow, error) {
rows, err := q.db.QueryContext(ctx, listNodeMetrics, arg.NodeID, arg.Timestamp, arg.Timestamp_2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListNodeMetricsRow{}
for rows.Next() {
var i ListNodeMetricsRow
if err := rows.Scan(
&i.NodeID,
&i.Timestamp,
&i.CpuUsage,
&i.CpuCores,
&i.LoadAvg1,
&i.LoadAvg5,
&i.LoadAvg15,
&i.MemoryTotal,
&i.MemoryUsed,
&i.MemoryAvailable,
&i.MemoryUsagePercent,
&i.StorageTotal,
&i.StorageUsed,
&i.StorageAvailable,
&i.StorageUsagePercent,
&i.NetworkBytesIn,
&i.NetworkBytesOut,
&i.NetworkPacketsIn,
&i.NetworkPacketsOut,
&i.NetworkConnectionsIn,
&i.NetworkConnectionsOut,
&i.NetworkErrorsIn,
&i.NetworkErrorsOut,
&i.UptimeSeconds,
&i.Processes,
&i.Os,
&i.Kernel,
&i.Architecture,
); 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 listServiceMetrics = `-- name: ListServiceMetrics :many
SELECT service_id, service_name, project_id, timestamp,
requests_total, requests_success, requests_errors, requests_avg_latency,
requests_p95_latency, requests_p99_latency, requests_throughput,
errors_total, errors_rate, performance_response_time, performance_throughput,
performance_concurrency, performance_saturation, performance_utilization,
resource_cpu_usage, resource_memory_usage, resource_storage_usage,
resource_network_usage, resource_score
FROM service_metrics
WHERE service_id = $1 AND timestamp BETWEEN $2 AND $3
ORDER BY timestamp ASC
`
type ListServiceMetricsParams struct {
ServiceID string `json:"service_id"`
Timestamp time.Time `json:"timestamp"`
Timestamp_2 time.Time `json:"timestamp_2"`
}
type ListServiceMetricsRow struct {
ServiceID string `json:"service_id"`
ServiceName string `json:"service_name"`
ProjectID string `json:"project_id"`
Timestamp time.Time `json:"timestamp"`
RequestsTotal sql.NullInt64 `json:"requests_total"`
RequestsSuccess sql.NullInt64 `json:"requests_success"`
RequestsErrors sql.NullInt64 `json:"requests_errors"`
RequestsAvgLatency sql.NullString `json:"requests_avg_latency"`
RequestsP95Latency sql.NullString `json:"requests_p95_latency"`
RequestsP99Latency sql.NullString `json:"requests_p99_latency"`
RequestsThroughput sql.NullString `json:"requests_throughput"`
ErrorsTotal sql.NullInt64 `json:"errors_total"`
ErrorsRate sql.NullString `json:"errors_rate"`
PerformanceResponseTime sql.NullString `json:"performance_response_time"`
PerformanceThroughput sql.NullString `json:"performance_throughput"`
PerformanceConcurrency sql.NullInt64 `json:"performance_concurrency"`
PerformanceSaturation sql.NullString `json:"performance_saturation"`
PerformanceUtilization sql.NullString `json:"performance_utilization"`
ResourceCpuUsage sql.NullString `json:"resource_cpu_usage"`
ResourceMemoryUsage sql.NullInt64 `json:"resource_memory_usage"`
ResourceStorageUsage sql.NullInt64 `json:"resource_storage_usage"`
ResourceNetworkUsage sql.NullInt64 `json:"resource_network_usage"`
ResourceScore sql.NullString `json:"resource_score"`
}
func (q *Queries) ListServiceMetrics(ctx context.Context, arg ListServiceMetricsParams) ([]ListServiceMetricsRow, error) {
rows, err := q.db.QueryContext(ctx, listServiceMetrics, arg.ServiceID, arg.Timestamp, arg.Timestamp_2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListServiceMetricsRow{}
for rows.Next() {
var i ListServiceMetricsRow
if err := rows.Scan(
&i.ServiceID,
&i.ServiceName,
&i.ProjectID,
&i.Timestamp,
&i.RequestsTotal,
&i.RequestsSuccess,
&i.RequestsErrors,
&i.RequestsAvgLatency,
&i.RequestsP95Latency,
&i.RequestsP99Latency,
&i.RequestsThroughput,
&i.ErrorsTotal,
&i.ErrorsRate,
&i.PerformanceResponseTime,
&i.PerformanceThroughput,
&i.PerformanceConcurrency,
&i.PerformanceSaturation,
&i.PerformanceUtilization,
&i.ResourceCpuUsage,
&i.ResourceMemoryUsage,
&i.ResourceStorageUsage,
&i.ResourceNetworkUsage,
&i.ResourceScore,
); 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 upsertInstanceMetrics = `-- name: UpsertInstanceMetrics :exec
INSERT INTO instance_metrics (
service_id, timestamp, instance_id, node_id, status, cpu, memory,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
start_time, last_seen, health_status, health_last_check, health_check_count, health_failure_count
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21
)
ON CONFLICT (service_id, timestamp, instance_id) DO UPDATE SET
node_id = EXCLUDED.node_id,
status = EXCLUDED.status,
cpu = EXCLUDED.cpu,
memory = EXCLUDED.memory,
network_bytes_in = EXCLUDED.network_bytes_in,
network_bytes_out = EXCLUDED.network_bytes_out,
network_packets_in = EXCLUDED.network_packets_in,
network_packets_out = EXCLUDED.network_packets_out,
network_connections_in = EXCLUDED.network_connections_in,
network_connections_out = EXCLUDED.network_connections_out,
network_errors_in = EXCLUDED.network_errors_in,
network_errors_out = EXCLUDED.network_errors_out,
start_time = EXCLUDED.start_time,
last_seen = EXCLUDED.last_seen,
health_status = EXCLUDED.health_status,
health_last_check = EXCLUDED.health_last_check,
health_check_count = EXCLUDED.health_check_count,
health_failure_count = EXCLUDED.health_failure_count
`
type UpsertInstanceMetricsParams struct {
ServiceID string `json:"service_id"`
Timestamp time.Time `json:"timestamp"`
InstanceID string `json:"instance_id"`
NodeID sql.NullString `json:"node_id"`
Status sql.NullString `json:"status"`
Cpu sql.NullString `json:"cpu"`
Memory sql.NullInt64 `json:"memory"`
NetworkBytesIn sql.NullInt64 `json:"network_bytes_in"`
NetworkBytesOut sql.NullInt64 `json:"network_bytes_out"`
NetworkPacketsIn sql.NullInt64 `json:"network_packets_in"`
NetworkPacketsOut sql.NullInt64 `json:"network_packets_out"`
NetworkConnectionsIn sql.NullInt32 `json:"network_connections_in"`
NetworkConnectionsOut sql.NullInt32 `json:"network_connections_out"`
NetworkErrorsIn sql.NullInt64 `json:"network_errors_in"`
NetworkErrorsOut sql.NullInt64 `json:"network_errors_out"`
StartTime sql.NullTime `json:"start_time"`
LastSeen sql.NullTime `json:"last_seen"`
HealthStatus sql.NullString `json:"health_status"`
HealthLastCheck sql.NullTime `json:"health_last_check"`
HealthCheckCount sql.NullInt32 `json:"health_check_count"`
HealthFailureCount sql.NullInt32 `json:"health_failure_count"`
}
func (q *Queries) UpsertInstanceMetrics(ctx context.Context, arg UpsertInstanceMetricsParams) error {
_, err := q.db.ExecContext(ctx, upsertInstanceMetrics,
arg.ServiceID,
arg.Timestamp,
arg.InstanceID,
arg.NodeID,
arg.Status,
arg.Cpu,
arg.Memory,
arg.NetworkBytesIn,
arg.NetworkBytesOut,
arg.NetworkPacketsIn,
arg.NetworkPacketsOut,
arg.NetworkConnectionsIn,
arg.NetworkConnectionsOut,
arg.NetworkErrorsIn,
arg.NetworkErrorsOut,
arg.StartTime,
arg.LastSeen,
arg.HealthStatus,
arg.HealthLastCheck,
arg.HealthCheckCount,
arg.HealthFailureCount,
)
return err
}
const upsertNodeMetrics = `-- name: UpsertNodeMetrics :exec
INSERT INTO node_metrics (
node_id, timestamp, cpu_usage, cpu_cores, load_avg_1, load_avg_5, load_avg_15,
memory_total, memory_used, memory_available, memory_usage_percent,
storage_total, storage_used, storage_available, storage_usage_percent,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
uptime, processes, os, kernel, architecture
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28
)
ON CONFLICT (node_id, timestamp) DO UPDATE SET
cpu_usage = EXCLUDED.cpu_usage,
cpu_cores = EXCLUDED.cpu_cores,
load_avg_1 = EXCLUDED.load_avg_1,
load_avg_5 = EXCLUDED.load_avg_5,
load_avg_15 = EXCLUDED.load_avg_15,
memory_total = EXCLUDED.memory_total,
memory_used = EXCLUDED.memory_used,
memory_available = EXCLUDED.memory_available,
memory_usage_percent = EXCLUDED.memory_usage_percent,
storage_total = EXCLUDED.storage_total,
storage_used = EXCLUDED.storage_used,
storage_available = EXCLUDED.storage_available,
storage_usage_percent = EXCLUDED.storage_usage_percent,
network_bytes_in = EXCLUDED.network_bytes_in,
network_bytes_out = EXCLUDED.network_bytes_out,
network_packets_in = EXCLUDED.network_packets_in,
network_packets_out = EXCLUDED.network_packets_out,
network_connections_in = EXCLUDED.network_connections_in,
network_connections_out = EXCLUDED.network_connections_out,
network_errors_in = EXCLUDED.network_errors_in,
network_errors_out = EXCLUDED.network_errors_out,
uptime = EXCLUDED.uptime,
processes = EXCLUDED.processes,
os = EXCLUDED.os,
kernel = EXCLUDED.kernel,
architecture = EXCLUDED.architecture
`
type UpsertNodeMetricsParams struct {
NodeID string `json:"node_id"`
Timestamp time.Time `json:"timestamp"`
CpuUsage sql.NullString `json:"cpu_usage"`
CpuCores sql.NullString `json:"cpu_cores"`
LoadAvg1 sql.NullString `json:"load_avg_1"`
LoadAvg5 sql.NullString `json:"load_avg_5"`
LoadAvg15 sql.NullString `json:"load_avg_15"`
MemoryTotal sql.NullInt64 `json:"memory_total"`
MemoryUsed sql.NullInt64 `json:"memory_used"`
MemoryAvailable sql.NullInt64 `json:"memory_available"`
MemoryUsagePercent sql.NullString `json:"memory_usage_percent"`
StorageTotal sql.NullInt64 `json:"storage_total"`
StorageUsed sql.NullInt64 `json:"storage_used"`
StorageAvailable sql.NullInt64 `json:"storage_available"`
StorageUsagePercent sql.NullString `json:"storage_usage_percent"`
NetworkBytesIn sql.NullInt64 `json:"network_bytes_in"`
NetworkBytesOut sql.NullInt64 `json:"network_bytes_out"`
NetworkPacketsIn sql.NullInt64 `json:"network_packets_in"`
NetworkPacketsOut sql.NullInt64 `json:"network_packets_out"`
NetworkConnectionsIn sql.NullInt32 `json:"network_connections_in"`
NetworkConnectionsOut sql.NullInt32 `json:"network_connections_out"`
NetworkErrorsIn sql.NullInt64 `json:"network_errors_in"`
NetworkErrorsOut sql.NullInt64 `json:"network_errors_out"`
Uptime sql.NullInt64 `json:"uptime"`
Processes sql.NullInt32 `json:"processes"`
Os sql.NullString `json:"os"`
Kernel sql.NullString `json:"kernel"`
Architecture sql.NullString `json:"architecture"`
}
func (q *Queries) UpsertNodeMetrics(ctx context.Context, arg UpsertNodeMetricsParams) error {
_, err := q.db.ExecContext(ctx, upsertNodeMetrics,
arg.NodeID,
arg.Timestamp,
arg.CpuUsage,
arg.CpuCores,
arg.LoadAvg1,
arg.LoadAvg5,
arg.LoadAvg15,
arg.MemoryTotal,
arg.MemoryUsed,
arg.MemoryAvailable,
arg.MemoryUsagePercent,
arg.StorageTotal,
arg.StorageUsed,
arg.StorageAvailable,
arg.StorageUsagePercent,
arg.NetworkBytesIn,
arg.NetworkBytesOut,
arg.NetworkPacketsIn,
arg.NetworkPacketsOut,
arg.NetworkConnectionsIn,
arg.NetworkConnectionsOut,
arg.NetworkErrorsIn,
arg.NetworkErrorsOut,
arg.Uptime,
arg.Processes,
arg.Os,
arg.Kernel,
arg.Architecture,
)
return err
}
const upsertServiceMetrics = `-- name: UpsertServiceMetrics :exec
INSERT INTO service_metrics (
service_id, service_name, project_id, timestamp,
requests_total, requests_success, requests_errors, requests_avg_latency,
requests_p95_latency, requests_p99_latency, requests_throughput,
errors_total, errors_rate, performance_response_time, performance_throughput,
performance_concurrency, performance_saturation, performance_utilization,
resource_cpu_usage, resource_memory_usage, resource_storage_usage,
resource_network_usage, resource_score
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21, $22, $23
)
ON CONFLICT (service_id, timestamp) DO UPDATE SET
requests_total = EXCLUDED.requests_total,
requests_success = EXCLUDED.requests_success,
requests_errors = EXCLUDED.requests_errors,
requests_avg_latency = EXCLUDED.requests_avg_latency,
requests_p95_latency = EXCLUDED.requests_p95_latency,
requests_p99_latency = EXCLUDED.requests_p99_latency,
requests_throughput = EXCLUDED.requests_throughput,
errors_total = EXCLUDED.errors_total,
errors_rate = EXCLUDED.errors_rate,
performance_response_time = EXCLUDED.performance_response_time,
performance_throughput = EXCLUDED.performance_throughput,
performance_concurrency = EXCLUDED.performance_concurrency,
performance_saturation = EXCLUDED.performance_saturation,
performance_utilization = EXCLUDED.performance_utilization,
resource_cpu_usage = EXCLUDED.resource_cpu_usage,
resource_memory_usage = EXCLUDED.resource_memory_usage,
resource_storage_usage = EXCLUDED.resource_storage_usage,
resource_network_usage = EXCLUDED.resource_network_usage,
resource_score = EXCLUDED.resource_score
`
type UpsertServiceMetricsParams struct {
ServiceID string `json:"service_id"`
ServiceName string `json:"service_name"`
ProjectID string `json:"project_id"`
Timestamp time.Time `json:"timestamp"`
RequestsTotal sql.NullInt64 `json:"requests_total"`
RequestsSuccess sql.NullInt64 `json:"requests_success"`
RequestsErrors sql.NullInt64 `json:"requests_errors"`
RequestsAvgLatency sql.NullString `json:"requests_avg_latency"`
RequestsP95Latency sql.NullString `json:"requests_p95_latency"`
RequestsP99Latency sql.NullString `json:"requests_p99_latency"`
RequestsThroughput sql.NullString `json:"requests_throughput"`
ErrorsTotal sql.NullInt64 `json:"errors_total"`
ErrorsRate sql.NullString `json:"errors_rate"`
PerformanceResponseTime sql.NullString `json:"performance_response_time"`
PerformanceThroughput sql.NullString `json:"performance_throughput"`
PerformanceConcurrency sql.NullInt64 `json:"performance_concurrency"`
PerformanceSaturation sql.NullString `json:"performance_saturation"`
PerformanceUtilization sql.NullString `json:"performance_utilization"`
ResourceCpuUsage sql.NullString `json:"resource_cpu_usage"`
ResourceMemoryUsage sql.NullInt64 `json:"resource_memory_usage"`
ResourceStorageUsage sql.NullInt64 `json:"resource_storage_usage"`
ResourceNetworkUsage sql.NullInt64 `json:"resource_network_usage"`
ResourceScore sql.NullString `json:"resource_score"`
}
func (q *Queries) UpsertServiceMetrics(ctx context.Context, arg UpsertServiceMetricsParams) error {
_, err := q.db.ExecContext(ctx, upsertServiceMetrics,
arg.ServiceID,
arg.ServiceName,
arg.ProjectID,
arg.Timestamp,
arg.RequestsTotal,
arg.RequestsSuccess,
arg.RequestsErrors,
arg.RequestsAvgLatency,
arg.RequestsP95Latency,
arg.RequestsP99Latency,
arg.RequestsThroughput,
arg.ErrorsTotal,
arg.ErrorsRate,
arg.PerformanceResponseTime,
arg.PerformanceThroughput,
arg.PerformanceConcurrency,
arg.PerformanceSaturation,
arg.PerformanceUtilization,
arg.ResourceCpuUsage,
arg.ResourceMemoryUsage,
arg.ResourceStorageUsage,
arg.ResourceNetworkUsage,
arg.ResourceScore,
)
return err
}
+50 -291
View File
@@ -78,28 +78,6 @@ type Banner struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type ComplianceFramework struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description sql.NullString `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ComplianceReport struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
FrameworkID uuid.UUID `json:"framework_id"`
AssessmentDate time.Time `json:"assessment_date"`
Assessor sql.NullString `json:"assessor"`
OverallStatus string `json:"overall_status"`
Score int32 `json:"score"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ContainerInstance struct { type ContainerInstance struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -120,34 +98,6 @@ type ContainerInstance struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type CronExecution struct {
ID uuid.UUID `json:"id"`
CronJobID uuid.UUID `json:"cron_job_id"`
StartedAt time.Time `json:"started_at"`
FinishedAt sql.NullTime `json:"finished_at"`
Status sql.NullString `json:"status"`
Output sql.NullString `json:"output"`
Error sql.NullString `json:"error"`
}
type CronJob struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
Name string `json:"name"`
Schedule string `json:"schedule"`
Command string `json:"command"`
Timezone sql.NullString `json:"timezone"`
Enabled sql.NullBool `json:"enabled"`
LastRunAt sql.NullTime `json:"last_run_at"`
NextRunAt sql.NullTime `json:"next_run_at"`
LastStatus sql.NullString `json:"last_status"`
LastOutput sql.NullString `json:"last_output"`
Retention sql.NullInt32 `json:"retention"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type DatabaseBackup struct { type DatabaseBackup struct {
ID string `json:"id"` ID string `json:"id"`
DatabaseID string `json:"database_id"` DatabaseID string `json:"database_id"`
@@ -186,22 +136,11 @@ 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"` Status sql.NullString `json:"status"`
CommitHash sql.NullString `json:"commit_hash"` CreatedAt sql.NullTime `json:"created_at"`
ImageDigest sql.NullString `json:"image_digest"` UpdatedAt sql.NullTime `json:"updated_at"`
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"`
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 {
@@ -222,71 +161,6 @@ type EnvironmentVariable struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type GitProvider struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
ApiUrl string `json:"api_url"`
WebhookUrl string `json:"webhook_url"`
AccessToken string `json:"access_token"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type GitRepository struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description sql.NullString `json:"description"`
CloneUrl string `json:"clone_url"`
WebhookUrl sql.NullString `json:"webhook_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type GitWebhook struct {
ID uuid.UUID `json:"id"`
RepoID uuid.UUID `json:"repo_id"`
ProviderID uuid.UUID `json:"provider_id"`
Events string `json:"events"`
WebhookSecret string `json:"webhook_secret"`
RemoteWebhookID sql.NullString `json:"remote_webhook_id"`
Active sql.NullBool `json:"active"`
BranchFilter sql.NullString `json:"branch_filter"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type InstanceMetric struct {
ServiceID string `json:"service_id"`
Timestamp time.Time `json:"timestamp"`
InstanceID string `json:"instance_id"`
NodeID sql.NullString `json:"node_id"`
Status sql.NullString `json:"status"`
Cpu sql.NullString `json:"cpu"`
Memory sql.NullInt64 `json:"memory"`
NetworkBytesIn sql.NullInt64 `json:"network_bytes_in"`
NetworkBytesOut sql.NullInt64 `json:"network_bytes_out"`
NetworkPacketsIn sql.NullInt64 `json:"network_packets_in"`
NetworkPacketsOut sql.NullInt64 `json:"network_packets_out"`
NetworkConnectionsIn sql.NullInt32 `json:"network_connections_in"`
NetworkConnectionsOut sql.NullInt32 `json:"network_connections_out"`
NetworkErrorsIn sql.NullInt64 `json:"network_errors_in"`
NetworkErrorsOut sql.NullInt64 `json:"network_errors_out"`
StartTime sql.NullTime `json:"start_time"`
LastSeen sql.NullTime `json:"last_seen"`
HealthStatus sql.NullString `json:"health_status"`
HealthLastCheck sql.NullTime `json:"health_last_check"`
HealthCheckCount sql.NullInt32 `json:"health_check_count"`
HealthFailureCount sql.NullInt32 `json:"health_failure_count"`
CreatedAt sql.NullTime `json:"created_at"`
}
type NodeAgent struct { type NodeAgent struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -307,38 +181,6 @@ type NodeAgent struct {
DefaultDomain string `json:"default_domain"` DefaultDomain string `json:"default_domain"`
} }
type NodeMetric struct {
NodeID string `json:"node_id"`
Timestamp time.Time `json:"timestamp"`
CpuUsage sql.NullString `json:"cpu_usage"`
CpuCores sql.NullString `json:"cpu_cores"`
LoadAvg1 sql.NullString `json:"load_avg_1"`
LoadAvg5 sql.NullString `json:"load_avg_5"`
LoadAvg15 sql.NullString `json:"load_avg_15"`
MemoryTotal sql.NullInt64 `json:"memory_total"`
MemoryUsed sql.NullInt64 `json:"memory_used"`
MemoryAvailable sql.NullInt64 `json:"memory_available"`
MemoryUsagePercent sql.NullString `json:"memory_usage_percent"`
StorageTotal sql.NullInt64 `json:"storage_total"`
StorageUsed sql.NullInt64 `json:"storage_used"`
StorageAvailable sql.NullInt64 `json:"storage_available"`
StorageUsagePercent sql.NullString `json:"storage_usage_percent"`
NetworkBytesIn sql.NullInt64 `json:"network_bytes_in"`
NetworkBytesOut sql.NullInt64 `json:"network_bytes_out"`
NetworkPacketsIn sql.NullInt64 `json:"network_packets_in"`
NetworkPacketsOut sql.NullInt64 `json:"network_packets_out"`
NetworkConnectionsIn sql.NullInt32 `json:"network_connections_in"`
NetworkConnectionsOut sql.NullInt32 `json:"network_connections_out"`
NetworkErrorsIn sql.NullInt64 `json:"network_errors_in"`
NetworkErrorsOut sql.NullInt64 `json:"network_errors_out"`
Uptime sql.NullInt64 `json:"uptime"`
Processes sql.NullInt32 `json:"processes"`
Os sql.NullString `json:"os"`
Kernel sql.NullString `json:"kernel"`
Architecture sql.NullString `json:"architecture"`
CreatedAt sql.NullTime `json:"created_at"`
}
type Notification struct { type Notification struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"` UserID uuid.UUID `json:"user_id"`
@@ -375,21 +217,6 @@ 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"`
@@ -419,67 +246,52 @@ type Registry struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
type SecurityScan struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.NullUUID `json:"service_id"`
ScanType string `json:"scan_type"`
Status string `json:"status"`
StartedAt time.Time `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
Summary pqtype.NullRawMessage `json:"summary"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Service struct { type Service struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description sql.NullString `json:"description"` Description sql.NullString `json:"description"`
ProjectID uuid.UUID `json:"project_id"` ProjectID uuid.UUID `json:"project_id"`
EnvironmentID uuid.UUID `json:"environment_id"` EnvironmentID uuid.UUID `json:"environment_id"`
ServiceType string `json:"service_type"` ServiceType string `json:"service_type"`
SourceType string `json:"source_type"` SourceType string `json:"source_type"`
SourceUrl sql.NullString `json:"source_url"` SourceUrl sql.NullString `json:"source_url"`
ImageName sql.NullString `json:"image_name"` ImageName sql.NullString `json:"image_name"`
BuildCommand sql.NullString `json:"build_command"` BuildCommand sql.NullString `json:"build_command"`
StartCommand sql.NullString `json:"start_command"` StartCommand sql.NullString `json:"start_command"`
CpuLimit sql.NullInt32 `json:"cpu_limit"` CpuLimit sql.NullInt32 `json:"cpu_limit"`
MemoryLimit sql.NullInt32 `json:"memory_limit"` MemoryLimit sql.NullInt32 `json:"memory_limit"`
PublicUrl sql.NullString `json:"public_url"` PublicUrl sql.NullString `json:"public_url"`
HealthCheckUrl sql.NullString `json:"health_check_url"` HealthCheckUrl sql.NullString `json:"health_check_url"`
Status sql.NullString `json:"status"` Status sql.NullString `json:"status"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
Type sql.NullString `json:"type"` Type sql.NullString `json:"type"`
Image sql.NullString `json:"image"` Image sql.NullString `json:"image"`
Command sql.NullString `json:"command"` Command sql.NullString `json:"command"`
Environment sql.NullString `json:"environment"` Environment sql.NullString `json:"environment"`
GitRepo sql.NullString `json:"git_repo"` GitRepo sql.NullString `json:"git_repo"`
GitBranch sql.NullString `json:"git_branch"` GitBranch sql.NullString `json:"git_branch"`
BuildPath sql.NullString `json:"build_path"` BuildPath sql.NullString `json:"build_path"`
Cpu sql.NullString `json:"cpu"` Cpu sql.NullString `json:"cpu"`
Memory sql.NullString `json:"memory"` Memory sql.NullString `json:"memory"`
Replicas int32 `json:"replicas"` Replicas int32 `json:"replicas"`
Port int32 `json:"port"` Port int32 `json:"port"`
Domain string `json:"domain"` Domain string `json:"domain"`
HealthcheckPath string `json:"healthcheck_path"` HealthcheckPath string `json:"healthcheck_path"`
RestartPolicy string `json:"restart_policy"` RestartPolicy string `json:"restart_policy"`
PublishedPort int32 `json:"published_port"` PublishedPort int32 `json:"published_port"`
Volumes json.RawMessage `json:"volumes"` Volumes json.RawMessage `json:"volumes"`
MaintenanceMode bool `json:"maintenance_mode"` MaintenanceMode bool `json:"maintenance_mode"`
BasicAuthUsers string `json:"basic_auth_users"` BasicAuthUsers string `json:"basic_auth_users"`
Builder string `json:"builder"` Builder string `json:"builder"`
CpuReserve string `json:"cpu_reserve"` CpuReserve string `json:"cpu_reserve"`
MemoryReserve string `json:"memory_reserve"` MemoryReserve string `json:"memory_reserve"`
StaticBuildCmd string `json:"static_build_cmd"` StaticBuildCmd string `json:"static_build_cmd"`
StaticDir string `json:"static_dir"` StaticDir string `json:"static_dir"`
NodeID sql.NullString `json:"node_id"` NodeID sql.NullString `json:"node_id"`
Spread bool `json:"spread"` Spread bool `json:"spread"`
SleepEnabled bool `json:"sleep_enabled"` PlacementTags json.RawMessage `json:"placement_tags"`
SleepIdleMinutes int32 `json:"sleep_idle_minutes"` TraefikLabels json.RawMessage `json:"traefik_labels"`
PlacementTags json.RawMessage `json:"placement_tags"`
TraefikLabels json.RawMessage `json:"traefik_labels"`
} }
type ServiceDomain struct { type ServiceDomain struct {
@@ -493,33 +305,6 @@ type ServiceDomain struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
type ServiceMetric struct {
ServiceID string `json:"service_id"`
ServiceName string `json:"service_name"`
ProjectID string `json:"project_id"`
Timestamp time.Time `json:"timestamp"`
RequestsTotal sql.NullInt64 `json:"requests_total"`
RequestsSuccess sql.NullInt64 `json:"requests_success"`
RequestsErrors sql.NullInt64 `json:"requests_errors"`
RequestsAvgLatency sql.NullString `json:"requests_avg_latency"`
RequestsP95Latency sql.NullString `json:"requests_p95_latency"`
RequestsP99Latency sql.NullString `json:"requests_p99_latency"`
RequestsThroughput sql.NullString `json:"requests_throughput"`
ErrorsTotal sql.NullInt64 `json:"errors_total"`
ErrorsRate sql.NullString `json:"errors_rate"`
PerformanceResponseTime sql.NullString `json:"performance_response_time"`
PerformanceThroughput sql.NullString `json:"performance_throughput"`
PerformanceConcurrency sql.NullInt64 `json:"performance_concurrency"`
PerformanceSaturation sql.NullString `json:"performance_saturation"`
PerformanceUtilization sql.NullString `json:"performance_utilization"`
ResourceCpuUsage sql.NullString `json:"resource_cpu_usage"`
ResourceMemoryUsage sql.NullInt64 `json:"resource_memory_usage"`
ResourceStorageUsage sql.NullInt64 `json:"resource_storage_usage"`
ResourceNetworkUsage sql.NullInt64 `json:"resource_network_usage"`
ResourceScore sql.NullString `json:"resource_score"`
CreatedAt sql.NullTime `json:"created_at"`
}
type ServiceTemplate struct { type ServiceTemplate struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -535,17 +320,6 @@ type ServiceTemplate struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type User struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
AvatarUrl sql.NullString `json:"avatar_url"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
IsAdmin bool `json:"is_admin"`
}
type UserInvite struct { type UserInvite struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
TokenHash string `json:"token_hash"` TokenHash string `json:"token_hash"`
@@ -570,21 +344,6 @@ type UserToken struct {
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
} }
type Vulnerability struct {
ID uuid.UUID `json:"id"`
Type string `json:"type"`
Severity string `json:"severity"`
Title string `json:"title"`
Description sql.NullString `json:"description"`
ServiceID uuid.NullUUID `json:"service_id"`
ProjectID uuid.UUID `json:"project_id"`
Status string `json:"status"`
FoundAt time.Time `json:"found_at"`
ResolvedAt sql.NullTime `json:"resolved_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WebhookDelivery struct { type WebhookDelivery struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
WebhookID uuid.UUID `json:"webhook_id"` WebhookID uuid.UUID `json:"webhook_id"`
@@ -1,635 +0,0 @@
// 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
}
@@ -13,33 +13,14 @@ import (
) )
type Querier interface { type Querier interface {
// Portable bucket bucketing (no TimescaleDB): floor epoch seconds into
// bucket-sized buckets, then back to timestamptz.
AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error)
CheckSecurityProjectAccess(ctx context.Context, arg CheckSecurityProjectAccessParams) (bool, 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
ComplianceFrameworkExists(ctx context.Context, id uuid.UUID) (bool, 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)
CountGitRepositories(ctx context.Context, arg CountGitRepositoriesParams) (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)
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)
CountUserRepositories(ctx context.Context, userID uuid.UUID) (int64, error)
CountUsers(ctx context.Context) (int64, error)
CountUsersByEmail(ctx context.Context, email string) (int64, error)
CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error) CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error)
CreateAgentAuthToken(ctx context.Context, arg CreateAgentAuthTokenParams) (AgentAuthToken, error) CreateAgentAuthToken(ctx context.Context, arg CreateAgentAuthTokenParams) (AgentAuthToken, error)
CreateBackupTarget(ctx context.Context, arg CreateBackupTargetParams) error CreateBackupTarget(ctx context.Context, arg CreateBackupTargetParams) error
@@ -51,10 +32,7 @@ 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)
CreateServiceFromTemplate(ctx context.Context, arg CreateServiceFromTemplateParams) error CreateServiceFromTemplate(ctx context.Context, arg CreateServiceFromTemplateParams) error
CreateUserInvite(ctx context.Context, arg CreateUserInviteParams) (UserInvite, error) CreateUserInvite(ctx context.Context, arg CreateUserInviteParams) (UserInvite, error)
CreateUserTemplate(ctx context.Context, arg CreateUserTemplateParams) error CreateUserTemplate(ctx context.Context, arg CreateUserTemplateParams) error
@@ -62,25 +40,17 @@ 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
DeleteCronJob(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
DeleteGitProvider(ctx context.Context, arg DeleteGitProviderParams) (int64, 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)
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
FinishCronExecution(ctx context.Context, arg FinishCronExecutionParams) 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)
@@ -88,82 +58,26 @@ type Querier interface {
GetBackupTargetByIDAndUser(ctx context.Context, arg GetBackupTargetByIDAndUserParams) (BackupTarget, error) GetBackupTargetByIDAndUser(ctx context.Context, arg GetBackupTargetByIDAndUserParams) (BackupTarget, error)
GetBannerByID(ctx context.Context, id uuid.UUID) (Banner, error) GetBannerByID(ctx context.Context, id uuid.UUID) (Banner, error)
GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error) GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error)
GetComplianceReportProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetContainer(ctx context.Context, id string) (ContainerInstance, error) GetContainer(ctx context.Context, id string) (ContainerInstance, error)
GetContainerForAgent(ctx context.Context, arg GetContainerForAgentParams) (ContainerInstance, error) GetContainerForAgent(ctx context.Context, arg GetContainerForAgentParams) (ContainerInstance, error)
GetCronJob(ctx context.Context, id uuid.UUID) (CronJob, error)
GetCronJobForTrigger(ctx context.Context, id uuid.UUID) (GetCronJobForTriggerRow, error)
GetCronJobOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetCronJobProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetCronOwnerViaService(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetDatabaseBackupByIDAndDatabaseAndUser(ctx context.Context, arg GetDatabaseBackupByIDAndDatabaseAndUserParams) (DatabaseBackup, error) GetDatabaseBackupByIDAndDatabaseAndUser(ctx context.Context, arg GetDatabaseBackupByIDAndDatabaseAndUserParams) (DatabaseBackup, error)
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)
GetGitProviderForFetch(ctx context.Context, arg GetGitProviderForFetchParams) (GetGitProviderForFetchRow, error)
GetGitProviderForRepos(ctx context.Context, arg GetGitProviderForReposParams) (GetGitProviderForReposRow, error)
GetGitProviderName(ctx context.Context, arg GetGitProviderNameParams) (string, error)
GetGitProviderNameByID(ctx context.Context, id uuid.UUID) (string, error)
GetGitProviderTimestamps(ctx context.Context, id uuid.UUID) (GetGitProviderTimestampsRow, error)
GetGitRepoForPush(ctx context.Context, id uuid.UUID) (GetGitRepoForPushRow, error)
GetGitRepositoryByFullName(ctx context.Context, arg GetGitRepositoryByFullNameParams) (GetGitRepositoryByFullNameRow, error)
GetGitRepositoryProviderID(ctx context.Context, arg GetGitRepositoryProviderIDParams) (uuid.UUID, error)
GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error)
GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error) GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error)
GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error)
GetLatestComplianceReport(ctx context.Context, projectID uuid.UUID) (GetLatestComplianceReportRow, error)
GetLatestSecurityScan(ctx context.Context, projectID uuid.UUID) (GetLatestSecurityScanRow, 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)
GetProjectOwnerText(ctx context.Context, id uuid.UUID) (string, error)
GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error) GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error)
GetSecurityScanProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error)
GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error)
GetServiceDomainFirst(ctx context.Context, serviceID uuid.UUID) (string, error)
GetServiceDomainText(ctx context.Context, id 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)
GetServicePort(ctx context.Context, id uuid.UUID) (int32, error)
GetServicePublishedPort(ctx context.Context, id uuid.UUID) (int32, error)
GetServiceRuntimeWithOwner(ctx context.Context, id uuid.UUID) (GetServiceRuntimeWithOwnerRow, error)
GetServiceStatusText(ctx context.Context, id uuid.UUID) (string, 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)
GetServiceVariableValue(ctx context.Context, arg GetServiceVariableValueParams) (string, error)
GetUserByEmailForAuth(ctx context.Context, email string) (GetUserByEmailForAuthRow, error)
GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, 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)
GetVulnerabilityMetrics(ctx context.Context, projectID uuid.UUID) (GetVulnerabilityMetricsRow, error)
GetVulnerabilityProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error
InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error
InsertCronJob(ctx context.Context, arg InsertCronJobParams) error
InsertDeployment(ctx context.Context, arg InsertDeploymentParams) error
InsertGitProvider(ctx context.Context, arg InsertGitProviderParams) error
InsertGitRepository(ctx context.Context, arg InsertGitRepositoryParams) (InsertGitRepositoryRow, 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
InsertUser(ctx context.Context, arg InsertUserParams) (InsertUserRow, error)
InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams) (InsertUserAdminRow, 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)
ListAgentHeartbeatsSince(ctx context.Context, arg ListAgentHeartbeatsSinceParams) ([]AgentHeartbeat, error) ListAgentHeartbeatsSince(ctx context.Context, arg ListAgentHeartbeatsSinceParams) ([]AgentHeartbeat, error)
@@ -173,36 +87,22 @@ type Querier interface {
ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error) ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error)
ListBanners(ctx context.Context) ([]Banner, error) ListBanners(ctx context.Context) ([]Banner, error)
ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error) ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error)
ListComplianceFrameworks(ctx context.Context) ([]ListComplianceFrameworksRow, error)
ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error)
ListContainersForAgent(ctx context.Context, nodeAgentID string) ([]ContainerInstance, error) ListContainersForAgent(ctx context.Context, nodeAgentID string) ([]ContainerInstance, error)
ListCronExecutions(ctx context.Context, cronJobID uuid.UUID) ([]CronExecution, error)
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)
ListDueCronJobs(ctx context.Context) ([]ListDueCronJobsRow, 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)
ListGitProviders(ctx context.Context, userID uuid.UUID) ([]ListGitProvidersRow, error)
ListGitRepositories(ctx context.Context, arg ListGitRepositoriesParams) ([]ListGitRepositoriesRow, error)
ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error)
ListNodeAgentsForService(ctx context.Context, serviceID string) ([]string, 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)
ListNotificationsByUser(ctx context.Context, arg ListNotificationsByUserParams) ([]Notification, error) ListNotificationsByUser(ctx context.Context, arg ListNotificationsByUserParams) ([]Notification, error)
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)
ListProjectServiceRefs(ctx context.Context, projectID uuid.UUID) ([]ListProjectServiceRefsRow, 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)
@@ -212,24 +112,14 @@ type Querier interface {
// Distinct agents holding inventory rows for a service. // Distinct agents holding inventory rows for a service.
ListServiceAgents(ctx context.Context, serviceID string) ([]string, error) ListServiceAgents(ctx context.Context, serviceID string) ([]string, error)
ListServiceContainers(ctx context.Context, serviceID string) ([]ListServiceContainersRow, error) ListServiceContainers(ctx context.Context, serviceID string) ([]ListServiceContainersRow, error)
ListServiceDomains(ctx context.Context, serviceID uuid.UUID) ([]ServiceDomain, 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)
ListServiceVariableValues(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariableValuesRow, error)
ListServiceVariablesWithSecret(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariablesWithSecretRow, error)
ListServicesForPush(ctx context.Context, arg ListServicesForPushParams) ([]ListServicesForPushRow, error)
ListSleepCandidates(ctx context.Context) ([]ListSleepCandidatesRow, 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)
ListVulnerabilities(ctx context.Context, projectID uuid.UUID) ([]ListVulnerabilitiesRow, 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)
@@ -239,8 +129,6 @@ type Querier interface {
RevokeAgentAuthToken(ctx context.Context, id uuid.UUID) (AgentAuthToken, error) RevokeAgentAuthToken(ctx context.Context, id uuid.UUID) (AgentAuthToken, error)
RevokeUserToken(ctx context.Context, arg RevokeUserTokenParams) (int64, error) RevokeUserToken(ctx context.Context, arg RevokeUserTokenParams) (int64, error)
ScrubCommandPayload(ctx context.Context, arg ScrubCommandPayloadParams) error ScrubCommandPayload(ctx context.Context, arg ScrubCommandPayloadParams) error
ServiceExistsInProject(ctx context.Context, arg ServiceExistsInProjectParams) (bool, error)
ServiceWriteAccess(ctx context.Context, arg ServiceWriteAccessParams) (bool, error)
SetAgentDefaultDomain(ctx context.Context, arg SetAgentDefaultDomainParams) error SetAgentDefaultDomain(ctx context.Context, arg SetAgentDefaultDomainParams) error
SetAgentSchedulable(ctx context.Context, arg SetAgentSchedulableParams) error SetAgentSchedulable(ctx context.Context, arg SetAgentSchedulableParams) error
SetAgentTags(ctx context.Context, arg SetAgentTagsParams) error SetAgentTags(ctx context.Context, arg SetAgentTagsParams) error
@@ -253,19 +141,9 @@ 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)
SetServiceLegacyDomain(ctx context.Context, arg SetServiceLegacyDomainParams) error
SetServicePublishedPort(ctx context.Context, arg SetServicePublishedPortParams) error
SetServiceReplicas(ctx context.Context, arg SetServiceReplicasParams) 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
TouchUserToken(ctx context.Context, id uuid.UUID) error TouchUserToken(ctx context.Context, id uuid.UUID) error
TrimCronExecutions(ctx context.Context, arg TrimCronExecutionsParams) error
UpdateAgent(ctx context.Context, arg UpdateAgentParams) (NodeAgent, error) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (NodeAgent, error)
UpdateAgentHeartbeat(ctx context.Context, arg UpdateAgentHeartbeatParams) error UpdateAgentHeartbeat(ctx context.Context, arg UpdateAgentHeartbeatParams) error
UpdateBackupTargetByIDAndUser(ctx context.Context, arg UpdateBackupTargetByIDAndUserParams) error UpdateBackupTargetByIDAndUser(ctx context.Context, arg UpdateBackupTargetByIDAndUserParams) error
@@ -278,27 +156,15 @@ type Querier interface {
// removed replica. $4 is the cumulative rx+tx byte counter. // removed replica. $4 is the cumulative rx+tx byte counter.
UpdateContainerStateByName(ctx context.Context, arg UpdateContainerStateByNameParams) error UpdateContainerStateByName(ctx context.Context, arg UpdateContainerStateByNameParams) error
UpdateContainerStatus(ctx context.Context, arg UpdateContainerStatusParams) error UpdateContainerStatus(ctx context.Context, arg UpdateContainerStatusParams) error
UpdateCronJobRun(ctx context.Context, arg UpdateCronJobRunParams) error
UpdateDatabaseServiceNameAndPlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameAndPlanByIDAndUserParams) error UpdateDatabaseServiceNameAndPlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameAndPlanByIDAndUserParams) error
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
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error) UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error)
UpdateVulnerabilityStatus(ctx context.Context, arg UpdateVulnerabilityStatusParams) 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
UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error)
UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error
UpsertGitWebhook(ctx context.Context, arg UpsertGitWebhookParams) (UpsertGitWebhookRow, error)
UpsertInstanceMetrics(ctx context.Context, arg UpsertInstanceMetricsParams) error
UpsertLocalUser(ctx context.Context, arg UpsertLocalUserParams) (UpsertLocalUserRow, error)
UpsertNodeMetrics(ctx context.Context, arg UpsertNodeMetricsParams) error
UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error
UpsertServiceMetrics(ctx context.Context, arg UpsertServiceMetricsParams) error
UpsertServiceTemplate(ctx context.Context, arg UpsertServiceTemplateParams) error UpsertServiceTemplate(ctx context.Context, arg UpsertServiceTemplateParams) error
} }
@@ -1,355 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: runtime.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const getLastDeployedImage = `-- name: GetLastDeployedImage :one
SELECT (image_name || ':' || image_tag)::text AS image FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1
`
func (q *Queries) GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getLastDeployedImage, serviceID)
var image string
err := row.Scan(&image)
return image, err
}
const getServiceDomainText = `-- name: GetServiceDomainText :one
SELECT COALESCE(domain,'') AS domain FROM services WHERE id = $1
`
func (q *Queries) GetServiceDomainText(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceDomainText, id)
var domain string
err := row.Scan(&domain)
return domain, err
}
const getServicePort = `-- name: GetServicePort :one
SELECT COALESCE(port, 0) AS port FROM services WHERE id = $1
`
func (q *Queries) GetServicePort(ctx context.Context, id uuid.UUID) (int32, error) {
row := q.db.QueryRowContext(ctx, getServicePort, id)
var port int32
err := row.Scan(&port)
return port, err
}
const getServiceRuntimeWithOwner = `-- name: GetServiceRuntimeWithOwner :one
SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status,
COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command,
COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo,
COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path,
COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS 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 GetServiceRuntimeWithOwnerRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
Image string `json:"image"`
Command string `json:"command"`
Environment string `json:"environment"`
GitRepo string `json:"git_repo"`
GitBranch string `json:"git_branch"`
BuildPath string `json:"build_path"`
Cpu string `json:"cpu"`
Memory string `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) GetServiceRuntimeWithOwner(ctx context.Context, id uuid.UUID) (GetServiceRuntimeWithOwnerRow, error) {
row := q.db.QueryRowContext(ctx, getServiceRuntimeWithOwner, id)
var i GetServiceRuntimeWithOwnerRow
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 getServiceStatusText = `-- name: GetServiceStatusText :one
SELECT COALESCE(status,'') AS status FROM services WHERE id = $1
`
func (q *Queries) GetServiceStatusText(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceStatusText, id)
var status string
err := row.Scan(&status)
return status, err
}
const getServiceVariableValue = `-- name: GetServiceVariableValue :one
SELECT value FROM environment_variables WHERE service_id = $1 AND key = $2
`
type GetServiceVariableValueParams struct {
ServiceID uuid.UUID `json:"service_id"`
Key string `json:"key"`
}
func (q *Queries) GetServiceVariableValue(ctx context.Context, arg GetServiceVariableValueParams) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceVariableValue, arg.ServiceID, arg.Key)
var value string
err := row.Scan(&value)
return value, err
}
const listNodeAgentsForService = `-- name: ListNodeAgentsForService :many
SELECT DISTINCT node_agent_id FROM container_instances WHERE service_id = $1
`
func (q *Queries) ListNodeAgentsForService(ctx context.Context, serviceID string) ([]string, error) {
rows, err := q.db.QueryContext(ctx, listNodeAgentsForService, serviceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []string{}
for rows.Next() {
var node_agent_id string
if err := rows.Scan(&node_agent_id); err != nil {
return nil, err
}
items = append(items, node_agent_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listProjectServiceRefs = `-- name: ListProjectServiceRefs :many
SELECT id, name FROM services WHERE project_id = $1
`
type ListProjectServiceRefsRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
}
func (q *Queries) ListProjectServiceRefs(ctx context.Context, projectID uuid.UUID) ([]ListProjectServiceRefsRow, error) {
rows, err := q.db.QueryContext(ctx, listProjectServiceRefs, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListProjectServiceRefsRow{}
for rows.Next() {
var i ListProjectServiceRefsRow
if err := rows.Scan(&i.ID, &i.Name); 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 listServiceVariableValues = `-- name: ListServiceVariableValues :many
SELECT key, value FROM environment_variables WHERE service_id = $1
`
type ListServiceVariableValuesRow struct {
Key string `json:"key"`
Value string `json:"value"`
}
func (q *Queries) ListServiceVariableValues(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariableValuesRow, error) {
rows, err := q.db.QueryContext(ctx, listServiceVariableValues, serviceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListServiceVariableValuesRow{}
for rows.Next() {
var i ListServiceVariableValuesRow
if err := rows.Scan(&i.Key, &i.Value); 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 listServiceVariablesWithSecret = `-- name: ListServiceVariablesWithSecret :many
SELECT key, value, COALESCE(is_secret, false) AS is_secret
FROM environment_variables WHERE service_id = $1
`
type ListServiceVariablesWithSecretRow struct {
Key string `json:"key"`
Value string `json:"value"`
IsSecret bool `json:"is_secret"`
}
func (q *Queries) ListServiceVariablesWithSecret(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariablesWithSecretRow, error) {
rows, err := q.db.QueryContext(ctx, listServiceVariablesWithSecret, serviceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListServiceVariablesWithSecretRow{}
for rows.Next() {
var i ListServiceVariablesWithSecretRow
if err := rows.Scan(&i.Key, &i.Value, &i.IsSecret); 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 listSleepCandidates = `-- name: ListSleepCandidates :many
SELECT id, project_id, name, COALESCE(status,'') AS status,
COALESCE(sleep_idle_minutes, 15) AS sleep_idle_minutes
FROM services
WHERE sleep_enabled AND COALESCE(status,'') IN ('running','deployed','degraded','starting')
`
type ListSleepCandidatesRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Status string `json:"status"`
SleepIdleMinutes int32 `json:"sleep_idle_minutes"`
}
func (q *Queries) ListSleepCandidates(ctx context.Context) ([]ListSleepCandidatesRow, error) {
rows, err := q.db.QueryContext(ctx, listSleepCandidates)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListSleepCandidatesRow{}
for rows.Next() {
var i ListSleepCandidatesRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Status,
&i.SleepIdleMinutes,
); 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 setServicePublishedPort = `-- name: SetServicePublishedPort :exec
UPDATE services SET published_port = $1 WHERE id = $2 AND published_port <> $1
`
type SetServicePublishedPortParams struct {
PublishedPort int32 `json:"published_port"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetServicePublishedPort(ctx context.Context, arg SetServicePublishedPortParams) error {
_, err := q.db.ExecContext(ctx, setServicePublishedPort, arg.PublishedPort, arg.ID)
return err
}
const setServiceReplicas = `-- name: SetServiceReplicas :exec
UPDATE services SET replicas = $1, updated_at = $2 WHERE id = $3
`
type SetServiceReplicasParams struct {
Replicas int32 `json:"replicas"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetServiceReplicas(ctx context.Context, arg SetServiceReplicasParams) error {
_, err := q.db.ExecContext(ctx, setServiceReplicas, arg.Replicas, arg.UpdatedAt, arg.ID)
return err
}
@@ -1,318 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: security.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const checkSecurityProjectAccess = `-- name: CheckSecurityProjectAccess :one
SELECT EXISTS (
SELECT 1
FROM projects p
WHERE p.id = $1
AND (p.is_approved
OR $3::bool
OR p.owner_id = $2 OR EXISTS (
SELECT 1 FROM project_members pm
WHERE pm.project_id = p.id AND pm.user_id = $2
))
)
`
type CheckSecurityProjectAccessParams struct {
ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"`
Column3 bool `json:"column_3"`
}
func (q *Queries) CheckSecurityProjectAccess(ctx context.Context, arg CheckSecurityProjectAccessParams) (bool, error) {
row := q.db.QueryRowContext(ctx, checkSecurityProjectAccess, arg.ID, arg.OwnerID, arg.Column3)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const complianceFrameworkExists = `-- name: ComplianceFrameworkExists :one
SELECT EXISTS(SELECT 1 FROM compliance_frameworks WHERE id = $1)
`
func (q *Queries) ComplianceFrameworkExists(ctx context.Context, id uuid.UUID) (bool, error) {
row := q.db.QueryRowContext(ctx, complianceFrameworkExists, id)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const getComplianceReportProjectID = `-- name: GetComplianceReportProjectID :one
SELECT project_id FROM compliance_reports WHERE id = $1
`
func (q *Queries) GetComplianceReportProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getComplianceReportProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getLatestComplianceReport = `-- name: GetLatestComplianceReport :one
SELECT overall_status, score, assessment_date
FROM compliance_reports
WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1
`
type GetLatestComplianceReportRow struct {
OverallStatus string `json:"overall_status"`
Score int32 `json:"score"`
AssessmentDate time.Time `json:"assessment_date"`
}
func (q *Queries) GetLatestComplianceReport(ctx context.Context, projectID uuid.UUID) (GetLatestComplianceReportRow, error) {
row := q.db.QueryRowContext(ctx, getLatestComplianceReport, projectID)
var i GetLatestComplianceReportRow
err := row.Scan(&i.OverallStatus, &i.Score, &i.AssessmentDate)
return i, err
}
const getLatestSecurityScan = `-- name: GetLatestSecurityScan :one
SELECT id, COALESCE((summary->>'score')::int, 0)::int AS score, started_at AS scanned_at, status
FROM security_scans
WHERE project_id = $1
ORDER BY started_at DESC
LIMIT 1
`
type GetLatestSecurityScanRow struct {
ID uuid.UUID `json:"id"`
Score int32 `json:"score"`
ScannedAt time.Time `json:"scanned_at"`
Status string `json:"status"`
}
func (q *Queries) GetLatestSecurityScan(ctx context.Context, projectID uuid.UUID) (GetLatestSecurityScanRow, error) {
row := q.db.QueryRowContext(ctx, getLatestSecurityScan, projectID)
var i GetLatestSecurityScanRow
err := row.Scan(
&i.ID,
&i.Score,
&i.ScannedAt,
&i.Status,
)
return i, err
}
const getProjectOwnerText = `-- name: GetProjectOwnerText :one
SELECT owner_id::text FROM projects WHERE id = $1
`
func (q *Queries) GetProjectOwnerText(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getProjectOwnerText, id)
var owner_id string
err := row.Scan(&owner_id)
return owner_id, err
}
const getSecurityScanProjectID = `-- name: GetSecurityScanProjectID :one
SELECT project_id FROM security_scans WHERE id = $1
`
func (q *Queries) GetSecurityScanProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getSecurityScanProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getVulnerabilityMetrics = `-- name: GetVulnerabilityMetrics :one
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE severity = 'critical') AS critical,
COUNT(*) FILTER (WHERE severity = 'high') AS high,
COUNT(*) FILTER (WHERE severity = 'medium') AS medium,
COUNT(*) FILTER (WHERE severity = 'low') AS low,
COUNT(*) FILTER (WHERE status = 'open') AS open,
COUNT(*) FILTER (WHERE status = 'resolved') AS resolved
FROM vulnerabilities
WHERE project_id = $1
`
type GetVulnerabilityMetricsRow struct {
Total int64 `json:"total"`
Critical int64 `json:"critical"`
High int64 `json:"high"`
Medium int64 `json:"medium"`
Low int64 `json:"low"`
Open int64 `json:"open"`
Resolved int64 `json:"resolved"`
}
func (q *Queries) GetVulnerabilityMetrics(ctx context.Context, projectID uuid.UUID) (GetVulnerabilityMetricsRow, error) {
row := q.db.QueryRowContext(ctx, getVulnerabilityMetrics, projectID)
var i GetVulnerabilityMetricsRow
err := row.Scan(
&i.Total,
&i.Critical,
&i.High,
&i.Medium,
&i.Low,
&i.Open,
&i.Resolved,
)
return i, err
}
const getVulnerabilityProjectID = `-- name: GetVulnerabilityProjectID :one
SELECT project_id FROM vulnerabilities WHERE id = $1
`
func (q *Queries) GetVulnerabilityProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getVulnerabilityProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const listComplianceFrameworks = `-- name: ListComplianceFrameworks :many
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name
`
type ListComplianceFrameworksRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description sql.NullString `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) ListComplianceFrameworks(ctx context.Context) ([]ListComplianceFrameworksRow, error) {
rows, err := q.db.QueryContext(ctx, listComplianceFrameworks)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListComplianceFrameworksRow{}
for rows.Next() {
var i ListComplianceFrameworksRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Version,
&i.Enabled,
&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 listVulnerabilities = `-- name: ListVulnerabilities :many
SELECT id, type, severity, title, description, service_id, status, found_at, resolved_at
FROM vulnerabilities
WHERE project_id = $1
ORDER BY
CASE severity
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
found_at DESC
`
type ListVulnerabilitiesRow struct {
ID uuid.UUID `json:"id"`
Type string `json:"type"`
Severity string `json:"severity"`
Title string `json:"title"`
Description sql.NullString `json:"description"`
ServiceID uuid.NullUUID `json:"service_id"`
Status string `json:"status"`
FoundAt time.Time `json:"found_at"`
ResolvedAt sql.NullTime `json:"resolved_at"`
}
func (q *Queries) ListVulnerabilities(ctx context.Context, projectID uuid.UUID) ([]ListVulnerabilitiesRow, error) {
rows, err := q.db.QueryContext(ctx, listVulnerabilities, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListVulnerabilitiesRow{}
for rows.Next() {
var i ListVulnerabilitiesRow
if err := rows.Scan(
&i.ID,
&i.Type,
&i.Severity,
&i.Title,
&i.Description,
&i.ServiceID,
&i.Status,
&i.FoundAt,
&i.ResolvedAt,
); 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 serviceExistsInProject = `-- name: ServiceExistsInProject :one
SELECT EXISTS(SELECT 1 FROM services WHERE id = $1 AND project_id = $2)
`
type ServiceExistsInProjectParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
}
func (q *Queries) ServiceExistsInProject(ctx context.Context, arg ServiceExistsInProjectParams) (bool, error) {
row := q.db.QueryRowContext(ctx, serviceExistsInProject, arg.ID, arg.ProjectID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const updateVulnerabilityStatus = `-- name: UpdateVulnerabilityStatus :exec
UPDATE vulnerabilities SET status = $1, resolved_at = $2 WHERE id = $3
`
type UpdateVulnerabilityStatusParams struct {
Status string `json:"status"`
ResolvedAt sql.NullTime `json:"resolved_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateVulnerabilityStatus(ctx context.Context, arg UpdateVulnerabilityStatusParams) error {
_, err := q.db.ExecContext(ctx, updateVulnerabilityStatus, arg.Status, arg.ResolvedAt, arg.ID)
return err
}
@@ -1,158 +0,0 @@
// 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()
}
+334 -261
View File
@@ -4,286 +4,293 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"strconv"
"sync" "sync"
"time" "time"
"containr/internal/database/sqlcdb"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )
// PostgreSQLMetricsStorage implements MetricsStorage using PostgreSQL // PostgreSQLMetricsStorage implements MetricsStorage using PostgreSQL
// through sqlc-generated queries (sqlc/queries/metrics.sql).
//
// Per-container node metrics are deliberately not persisted: the
// container_metrics table belongs to the container_instances model and
// has no place for the node/container samples collected here. Node and
// service/instance rows map cleanly, so those are stored.
type PostgreSQLMetricsStorage struct { type PostgreSQLMetricsStorage struct {
q *sqlcdb.Queries db *sql.DB
} }
// NewPostgreSQLMetricsStorage creates a new PostgreSQL metrics storage // NewPostgreSQLMetricsStorage creates a new PostgreSQL metrics storage
func NewPostgreSQLMetricsStorage(db *sql.DB) *PostgreSQLMetricsStorage { func NewPostgreSQLMetricsStorage(db *sql.DB) *PostgreSQLMetricsStorage {
return &PostgreSQLMetricsStorage{q: sqlcdb.New(db)} return &PostgreSQLMetricsStorage{db: db}
} }
func numStr(v float64) sql.NullString {
return sql.NullString{String: strconv.FormatFloat(v, 'f', -1, 64), Valid: true}
}
func int64N(v int64) sql.NullInt64 {
return sql.NullInt64{Int64: v, Valid: true}
}
func int32N(v int64) sql.NullInt32 {
return sql.NullInt32{Int32: int32(v), Valid: true}
}
func strN(v string) sql.NullString {
return sql.NullString{String: v, Valid: v != ""}
}
func timeN(t time.Time) sql.NullTime {
return sql.NullTime{Time: t, Valid: !t.IsZero()}
}
func f64v(n sql.NullString) float64 {
f, _ := strconv.ParseFloat(n.String, 64)
return f
}
func i64v(n sql.NullInt64) int64 { return n.Int64 }
func i32v(n sql.NullInt32) int64 { return int64(n.Int32) }
// StoreNodeMetrics stores node metrics in the database // StoreNodeMetrics stores node metrics in the database
func (s *PostgreSQLMetricsStorage) StoreNodeMetrics(ctx context.Context, metrics *NodeMetrics) error { func (s *PostgreSQLMetricsStorage) StoreNodeMetrics(ctx context.Context, metrics *NodeMetrics) error {
err := s.q.UpsertNodeMetrics(ctx, sqlcdb.UpsertNodeMetricsParams{ query := `
NodeID: metrics.NodeID, INSERT INTO node_metrics (
Timestamp: metrics.Timestamp, node_id, timestamp, cpu_usage, cpu_cores, load_avg_1, load_avg_5, load_avg_15,
CpuUsage: numStr(metrics.CPU.UsagePercent), memory_total, memory_used, memory_available, memory_usage_percent,
CpuCores: numStr(metrics.CPU.UsageCores), storage_total, storage_used, storage_available, storage_usage_percent,
LoadAvg1: numStr(metrics.CPU.LoadAverage1), network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
LoadAvg5: numStr(metrics.CPU.LoadAverage5), network_connections_in, network_connections_out, network_errors_in, network_errors_out,
LoadAvg15: numStr(metrics.CPU.LoadAverage15), uptime, processes, os, kernel, architecture
MemoryTotal: int64N(metrics.Memory.Total), ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)
MemoryUsed: int64N(metrics.Memory.Used), ON CONFLICT (node_id, timestamp) DO UPDATE SET
MemoryAvailable: int64N(metrics.Memory.Available), cpu_usage = EXCLUDED.cpu_usage,
MemoryUsagePercent: numStr(metrics.Memory.UsagePercent), cpu_cores = EXCLUDED.cpu_cores,
StorageTotal: int64N(metrics.Storage.Total), load_avg_1 = EXCLUDED.load_avg_1,
StorageUsed: int64N(metrics.Storage.Used), load_avg_5 = EXCLUDED.load_avg_5,
StorageAvailable: int64N(metrics.Storage.Available), load_avg_15 = EXCLUDED.load_avg_15,
StorageUsagePercent: numStr(metrics.Storage.UsagePercent), memory_total = EXCLUDED.memory_total,
NetworkBytesIn: int64N(metrics.Network.BytesIn), memory_used = EXCLUDED.memory_used,
NetworkBytesOut: int64N(metrics.Network.BytesOut), memory_available = EXCLUDED.memory_available,
NetworkPacketsIn: int64N(metrics.Network.PacketsIn), memory_usage_percent = EXCLUDED.memory_usage_percent,
NetworkPacketsOut: int64N(metrics.Network.PacketsOut), storage_total = EXCLUDED.storage_total,
NetworkConnectionsIn: int32N(metrics.Network.ConnectionsIn), storage_used = EXCLUDED.storage_used,
NetworkConnectionsOut: int32N(metrics.Network.ConnectionsOut), storage_available = EXCLUDED.storage_available,
NetworkErrorsIn: int64N(metrics.Network.ErrorsIn), storage_usage_percent = EXCLUDED.storage_usage_percent,
NetworkErrorsOut: int64N(metrics.Network.ErrorsOut), network_bytes_in = EXCLUDED.network_bytes_in,
// INTERVAL param — Postgres casts the bigint as seconds. network_bytes_out = EXCLUDED.network_bytes_out,
Uptime: int64N(int64(metrics.System.Uptime.Seconds())), network_packets_in = EXCLUDED.network_packets_in,
Processes: sql.NullInt32{Int32: int32(metrics.System.Processes), Valid: true}, network_packets_out = EXCLUDED.network_packets_out,
Os: strN(metrics.System.OS), network_connections_in = EXCLUDED.network_connections_in,
Kernel: strN(metrics.System.Kernel), network_connections_out = EXCLUDED.network_connections_out,
Architecture: strN(metrics.System.Architecture), network_errors_in = EXCLUDED.network_errors_in,
}) network_errors_out = EXCLUDED.network_errors_out,
uptime = EXCLUDED.uptime,
processes = EXCLUDED.processes,
os = EXCLUDED.os,
kernel = EXCLUDED.kernel,
architecture = EXCLUDED.architecture
`
_, err := s.db.ExecContext(ctx, query,
metrics.NodeID, metrics.Timestamp, metrics.CPU.UsagePercent, metrics.CPU.UsageCores,
metrics.CPU.LoadAverage1, metrics.CPU.LoadAverage5, metrics.CPU.LoadAverage15,
metrics.Memory.Total, metrics.Memory.Used, metrics.Memory.Available, metrics.Memory.UsagePercent,
metrics.Storage.Total, metrics.Storage.Used, metrics.Storage.Available, metrics.Storage.UsagePercent,
metrics.Network.BytesIn, metrics.Network.BytesOut, metrics.Network.PacketsIn, metrics.Network.PacketsOut,
metrics.Network.ConnectionsIn, metrics.Network.ConnectionsOut, metrics.Network.ErrorsIn, metrics.Network.ErrorsOut,
metrics.System.Uptime, metrics.System.Processes, metrics.System.OS, metrics.System.Kernel, metrics.System.Architecture,
)
if err != nil { if err != nil {
return fmt.Errorf("failed to store node metrics: %w", err) return fmt.Errorf("failed to store node metrics: %w", err)
} }
// Store container metrics
for _, container := range metrics.Containers {
if err := s.storeContainerMetrics(ctx, metrics.NodeID, metrics.Timestamp, container); err != nil {
return fmt.Errorf("failed to store container metrics: %w", err)
}
}
return nil return nil
} }
// StoreServiceMetrics stores service metrics in the database // StoreServiceMetrics stores service metrics in the database
func (s *PostgreSQLMetricsStorage) StoreServiceMetrics(ctx context.Context, metrics *ServiceMetrics) error { func (s *PostgreSQLMetricsStorage) StoreServiceMetrics(ctx context.Context, metrics *ServiceMetrics) error {
err := s.q.UpsertServiceMetrics(ctx, sqlcdb.UpsertServiceMetricsParams{ query := `
ServiceID: metrics.ServiceID, INSERT INTO service_metrics (
ServiceName: metrics.ServiceName, service_id, service_name, project_id, timestamp,
ProjectID: metrics.ProjectID, requests_total, requests_success, requests_errors, requests_avg_latency,
Timestamp: metrics.Timestamp, requests_p95_latency, requests_p99_latency, requests_throughput,
RequestsTotal: int64N(metrics.Requests.Total), errors_total, errors_rate, performance_response_time, performance_throughput,
RequestsSuccess: int64N(metrics.Requests.Success), performance_concurrency, performance_saturation, performance_utilization,
RequestsErrors: int64N(metrics.Requests.Errors), resource_cpu_usage, resource_memory_usage, resource_storage_usage,
RequestsAvgLatency: numStr(metrics.Requests.AvgLatency), resource_network_usage, resource_score
RequestsP95Latency: numStr(metrics.Requests.P95Latency), ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
RequestsP99Latency: numStr(metrics.Requests.P99Latency), ON CONFLICT (service_id, timestamp) DO UPDATE SET
RequestsThroughput: numStr(metrics.Requests.Throughput), requests_total = EXCLUDED.requests_total,
ErrorsTotal: int64N(metrics.Errors.Total), requests_success = EXCLUDED.requests_success,
ErrorsRate: numStr(metrics.Errors.Rate), requests_errors = EXCLUDED.requests_errors,
PerformanceResponseTime: numStr(metrics.Performance.ResponseTime), requests_avg_latency = EXCLUDED.requests_avg_latency,
PerformanceThroughput: numStr(metrics.Performance.Throughput), requests_p95_latency = EXCLUDED.requests_p95_latency,
PerformanceConcurrency: int64N(metrics.Performance.Concurrency), requests_p99_latency = EXCLUDED.requests_p99_latency,
PerformanceSaturation: numStr(metrics.Performance.Saturation), requests_throughput = EXCLUDED.requests_throughput,
PerformanceUtilization: numStr(metrics.Performance.Utilization), errors_total = EXCLUDED.errors_total,
ResourceCpuUsage: numStr(metrics.Resources.CPUUsage), errors_rate = EXCLUDED.errors_rate,
ResourceMemoryUsage: int64N(metrics.Resources.MemoryUsage), performance_response_time = EXCLUDED.performance_response_time,
ResourceStorageUsage: int64N(metrics.Resources.StorageUsage), performance_throughput = EXCLUDED.performance_throughput,
ResourceNetworkUsage: int64N(metrics.Resources.NetworkUsage), performance_concurrency = EXCLUDED.performance_concurrency,
ResourceScore: numStr(metrics.Resources.ResourceScore), performance_saturation = EXCLUDED.performance_saturation,
}) performance_utilization = EXCLUDED.performance_utilization,
resource_cpu_usage = EXCLUDED.resource_cpu_usage,
resource_memory_usage = EXCLUDED.resource_memory_usage,
resource_storage_usage = EXCLUDED.resource_storage_usage,
resource_network_usage = EXCLUDED.resource_network_usage,
resource_score = EXCLUDED.resource_score
`
_, err := s.db.ExecContext(ctx, query,
metrics.ServiceID, metrics.ServiceName, metrics.ProjectID, metrics.Timestamp,
metrics.Requests.Total, metrics.Requests.Success, metrics.Requests.Errors,
metrics.Requests.AvgLatency, metrics.Requests.P95Latency, metrics.Requests.P99Latency,
metrics.Requests.Throughput, metrics.Errors.Total, metrics.Errors.Rate,
metrics.Performance.ResponseTime, metrics.Performance.Throughput,
metrics.Performance.Concurrency, metrics.Performance.Saturation, metrics.Performance.Utilization,
metrics.Resources.CPUUsage, metrics.Resources.MemoryUsage, metrics.Resources.StorageUsage,
metrics.Resources.NetworkUsage, metrics.Resources.ResourceScore,
)
if err != nil { if err != nil {
return fmt.Errorf("failed to store service metrics: %w", err) return fmt.Errorf("failed to store service metrics: %w", err)
} }
// The service row must land first — instance_metrics references // Store instance metrics
// service_metrics(service_id, timestamp).
for _, instance := range metrics.Instances { for _, instance := range metrics.Instances {
if err := s.storeInstanceMetrics(ctx, metrics.ServiceID, metrics.Timestamp, instance); err != nil { if err := s.storeInstanceMetrics(ctx, metrics.ServiceID, metrics.Timestamp, instance); err != nil {
return fmt.Errorf("failed to store instance metrics: %w", err) return fmt.Errorf("failed to store instance metrics: %w", err)
} }
} }
return nil return nil
} }
// GetNodeMetrics retrieves node metrics from the database // GetNodeMetrics retrieves node metrics from the database
func (s *PostgreSQLMetricsStorage) GetNodeMetrics(ctx context.Context, nodeID string, from, to time.Time) ([]*NodeMetrics, error) { func (s *PostgreSQLMetricsStorage) GetNodeMetrics(ctx context.Context, nodeID string, from, to time.Time) ([]*NodeMetrics, error) {
rows, err := s.q.ListNodeMetrics(ctx, sqlcdb.ListNodeMetricsParams{ query := `
NodeID: nodeID, SELECT node_id, timestamp, cpu_usage, cpu_cores, load_avg_1, load_avg_5, load_avg_15,
Timestamp: from, memory_total, memory_used, memory_available, memory_usage_percent,
Timestamp_2: to, storage_total, storage_used, storage_available, storage_usage_percent,
}) network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
uptime, processes, os, kernel, architecture
FROM node_metrics
WHERE node_id = $1 AND timestamp BETWEEN $2 AND $3
ORDER BY timestamp ASC
`
rows, err := s.db.QueryContext(ctx, query, nodeID, from, to)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query node metrics: %w", err) return nil, fmt.Errorf("failed to query node metrics: %w", err)
} }
defer rows.Close()
var metrics []*NodeMetrics var metrics []*NodeMetrics
for _, r := range rows { for rows.Next() {
metrics = append(metrics, &NodeMetrics{ var m NodeMetrics
NodeID: r.NodeID, err := rows.Scan(
Timestamp: r.Timestamp, &m.NodeID, &m.Timestamp, &m.CPU.UsagePercent, &m.CPU.UsageCores,
CPU: CPUMetrics{ &m.CPU.LoadAverage1, &m.CPU.LoadAverage5, &m.CPU.LoadAverage15,
UsagePercent: f64v(r.CpuUsage), &m.Memory.Total, &m.Memory.Used, &m.Memory.Available, &m.Memory.UsagePercent,
UsageCores: f64v(r.CpuCores), &m.Storage.Total, &m.Storage.Used, &m.Storage.Available, &m.Storage.UsagePercent,
LoadAverage1: f64v(r.LoadAvg1), &m.Network.BytesIn, &m.Network.BytesOut, &m.Network.PacketsIn, &m.Network.PacketsOut,
LoadAverage5: f64v(r.LoadAvg5), &m.Network.ConnectionsIn, &m.Network.ConnectionsOut, &m.Network.ErrorsIn, &m.Network.ErrorsOut,
LoadAverage15: f64v(r.LoadAvg15), &m.System.Uptime, &m.System.Processes, &m.System.OS, &m.System.Kernel, &m.System.Architecture,
}, )
Memory: MemoryMetrics{ if err != nil {
Total: i64v(r.MemoryTotal), return nil, fmt.Errorf("failed to scan node metrics: %w", err)
Used: i64v(r.MemoryUsed), }
Available: i64v(r.MemoryAvailable),
UsagePercent: f64v(r.MemoryUsagePercent), // Get container metrics for this timestamp
}, containers, err := s.getContainerMetrics(ctx, nodeID, m.Timestamp)
Storage: StorageMetrics{ if err != nil {
Total: i64v(r.StorageTotal), return nil, fmt.Errorf("failed to get container metrics: %w", err)
Used: i64v(r.StorageUsed), }
Available: i64v(r.StorageAvailable), m.Containers = containers
UsagePercent: f64v(r.StorageUsagePercent),
}, metrics = append(metrics, &m)
Network: NetworkMetrics{
BytesIn: i64v(r.NetworkBytesIn),
BytesOut: i64v(r.NetworkBytesOut),
PacketsIn: i64v(r.NetworkPacketsIn),
PacketsOut: i64v(r.NetworkPacketsOut),
ConnectionsIn: i32v(r.NetworkConnectionsIn),
ConnectionsOut: i32v(r.NetworkConnectionsOut),
ErrorsIn: i64v(r.NetworkErrorsIn),
ErrorsOut: i64v(r.NetworkErrorsOut),
},
System: SystemMetrics{
Uptime: time.Duration(r.UptimeSeconds) * time.Second,
Processes: int(r.Processes.Int32),
OS: r.Os.String,
Kernel: r.Kernel.String,
Architecture: r.Architecture.String,
},
})
} }
return metrics, nil return metrics, nil
} }
// GetServiceMetrics retrieves service metrics from the database // GetServiceMetrics retrieves service metrics from the database
func (s *PostgreSQLMetricsStorage) GetServiceMetrics(ctx context.Context, serviceID string, from, to time.Time) ([]*ServiceMetrics, error) { func (s *PostgreSQLMetricsStorage) GetServiceMetrics(ctx context.Context, serviceID string, from, to time.Time) ([]*ServiceMetrics, error) {
rows, err := s.q.ListServiceMetrics(ctx, sqlcdb.ListServiceMetricsParams{ query := `
ServiceID: serviceID, SELECT service_id, service_name, project_id, timestamp,
Timestamp: from, requests_total, requests_success, requests_errors, requests_avg_latency,
Timestamp_2: to, requests_p95_latency, requests_p99_latency, requests_throughput,
}) errors_total, errors_rate, performance_response_time, performance_throughput,
performance_concurrency, performance_saturation, performance_utilization,
resource_cpu_usage, resource_memory_usage, resource_storage_usage,
resource_network_usage, resource_score
FROM service_metrics
WHERE service_id = $1 AND timestamp BETWEEN $2 AND $3
ORDER BY timestamp ASC
`
rows, err := s.db.QueryContext(ctx, query, serviceID, from, to)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query service metrics: %w", err) return nil, fmt.Errorf("failed to query service metrics: %w", err)
} }
defer rows.Close()
var metrics []*ServiceMetrics var metrics []*ServiceMetrics
for _, r := range rows { for rows.Next() {
m := &ServiceMetrics{ var m ServiceMetrics
ServiceID: r.ServiceID, err := rows.Scan(
ServiceName: r.ServiceName, &m.ServiceID, &m.ServiceName, &m.ProjectID, &m.Timestamp,
ProjectID: r.ProjectID, &m.Requests.Total, &m.Requests.Success, &m.Requests.Errors,
Timestamp: r.Timestamp, &m.Requests.AvgLatency, &m.Requests.P95Latency, &m.Requests.P99Latency,
Requests: RequestMetrics{ &m.Requests.Throughput, &m.Errors.Total, &m.Errors.Rate,
Total: i64v(r.RequestsTotal), &m.Performance.ResponseTime, &m.Performance.Throughput,
Success: i64v(r.RequestsSuccess), &m.Performance.Concurrency, &m.Performance.Saturation, &m.Performance.Utilization,
Errors: i64v(r.RequestsErrors), &m.Resources.CPUUsage, &m.Resources.MemoryUsage, &m.Resources.StorageUsage,
AvgLatency: f64v(r.RequestsAvgLatency), &m.Resources.NetworkUsage, &m.Resources.ResourceScore,
P95Latency: f64v(r.RequestsP95Latency), )
P99Latency: f64v(r.RequestsP99Latency), if err != nil {
Throughput: f64v(r.RequestsThroughput), return nil, fmt.Errorf("failed to scan service metrics: %w", err)
},
Errors: ErrorMetrics{
Total: i64v(r.ErrorsTotal),
Rate: f64v(r.ErrorsRate),
},
Performance: PerformanceMetrics{
ResponseTime: f64v(r.PerformanceResponseTime),
Throughput: f64v(r.PerformanceThroughput),
Concurrency: i64v(r.PerformanceConcurrency),
Saturation: f64v(r.PerformanceSaturation),
Utilization: f64v(r.PerformanceUtilization),
},
Resources: ResourceMetrics{
CPUUsage: f64v(r.ResourceCpuUsage),
MemoryUsage: i64v(r.ResourceMemoryUsage),
StorageUsage: i64v(r.ResourceStorageUsage),
NetworkUsage: i64v(r.ResourceNetworkUsage),
ResourceScore: f64v(r.ResourceScore),
},
} }
instances, err := s.getInstanceMetrics(ctx, serviceID, r.Timestamp) // Get instance metrics for this timestamp
instances, err := s.getInstanceMetrics(ctx, serviceID, m.Timestamp)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get instance metrics: %w", err) return nil, fmt.Errorf("failed to get instance metrics: %w", err)
} }
m.Instances = instances m.Instances = instances
metrics = append(metrics, m)
metrics = append(metrics, &m)
} }
return metrics, nil return metrics, nil
} }
// GetAggregatedMetrics retrieves aggregated metrics based on a query. // GetAggregatedMetrics retrieves aggregated metrics based on a query
// Bucketing uses date_bin (PostgreSQL 14+) — no TimescaleDB needed.
func (s *PostgreSQLMetricsStorage) GetAggregatedMetrics(ctx context.Context, query MetricsQuery) (*AggregatedMetrics, error) { func (s *PostgreSQLMetricsStorage) GetAggregatedMetrics(ctx context.Context, query MetricsQuery) (*AggregatedMetrics, error) {
// This is a simplified implementation
// In a real system, you'd build dynamic SQL based on the query
var timeSeries []TimeSeriesPoint var timeSeries []TimeSeriesPoint
var summary map[string]MetricSummary var summary map[string]MetricSummary
switch query.Type { switch query.Type {
case "node": case "node":
seconds := int64(query.Interval.Seconds()) // Aggregate node metrics
if seconds <= 0 { nodeQuery := `
seconds = 300 SELECT
} time_bucket($1, timestamp) AS bucket,
rows, err := s.q.AggregateNodeMetrics(ctx, sqlcdb.AggregateNodeMetricsParams{ AVG(cpu_usage) as avg_cpu,
Column1: seconds, AVG(memory_usage_percent) as avg_memory,
NodeID: query.ID, AVG(storage_usage_percent) as avg_storage
Timestamp: query.From, FROM node_metrics
Timestamp_2: query.To, WHERE node_id = $2 AND timestamp BETWEEN $3 AND $4
}) GROUP BY bucket
ORDER BY bucket ASC
`
rows, err := s.db.QueryContext(ctx, nodeQuery, query.Interval, query.ID, query.From, query.To)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query aggregated node metrics: %w", err) return nil, fmt.Errorf("failed to query aggregated node metrics: %w", err)
} }
for _, r := range rows { defer rows.Close()
bucket, _ := r.Bucket.(time.Time)
timeSeries = append(timeSeries, TimeSeriesPoint{ for rows.Next() {
var bucket time.Time
var avgCPU, avgMemory, avgStorage float64
if err := rows.Scan(&bucket, &avgCPU, &avgMemory, &avgStorage); err != nil {
return nil, fmt.Errorf("failed to scan aggregated metrics: %w", err)
}
point := TimeSeriesPoint{
Timestamp: bucket, Timestamp: bucket,
Values: map[string]float64{ Values: map[string]float64{
"cpu_usage": r.AvgCpu, "cpu_usage": avgCPU,
"memory_usage": r.AvgMemory, "memory_usage": avgMemory,
"storage_usage": r.AvgStorage, "storage_usage": avgStorage,
}, },
}) }
timeSeries = append(timeSeries, point)
} }
// Calculate summary statistics
summary = map[string]MetricSummary{ summary = map[string]MetricSummary{
"cpu_usage": calculateSummary(timeSeries, "cpu_usage"), "cpu_usage": calculateSummary(timeSeries, "cpu_usage"),
"memory_usage": calculateSummary(timeSeries, "memory_usage"), "memory_usage": calculateSummary(timeSeries, "memory_usage"),
@@ -298,69 +305,137 @@ func (s *PostgreSQLMetricsStorage) GetAggregatedMetrics(ctx context.Context, que
}, nil }, nil
} }
func (s *PostgreSQLMetricsStorage) storeInstanceMetrics(ctx context.Context, serviceID string, timestamp time.Time, instance InstanceMetrics) error { // Helper methods
return s.q.UpsertInstanceMetrics(ctx, sqlcdb.UpsertInstanceMetricsParams{
ServiceID: serviceID, func (s *PostgreSQLMetricsStorage) storeContainerMetrics(ctx context.Context, nodeID string, timestamp time.Time, container ContainerMetrics) error {
Timestamp: timestamp, query := `
InstanceID: instance.InstanceID, INSERT INTO container_metrics (
NodeID: strN(instance.NodeID), node_id, timestamp, container_id, name, state, cpu, memory,
Status: strN(instance.Status), network_bytes_in, network_bytes_out, network_packets_in, network_packets_out, start_time
Cpu: numStr(instance.CPU), ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
Memory: int64N(instance.Memory), ON CONFLICT (node_id, timestamp, container_id) DO UPDATE SET
NetworkBytesIn: int64N(instance.Network.BytesIn), name = EXCLUDED.name,
NetworkBytesOut: int64N(instance.Network.BytesOut), state = EXCLUDED.state,
NetworkPacketsIn: int64N(instance.Network.PacketsIn), cpu = EXCLUDED.cpu,
NetworkPacketsOut: int64N(instance.Network.PacketsOut), memory = EXCLUDED.memory,
NetworkConnectionsIn: int32N(instance.Network.ConnectionsIn), network_bytes_in = EXCLUDED.network_bytes_in,
NetworkConnectionsOut: int32N(instance.Network.ConnectionsOut), network_bytes_out = EXCLUDED.network_bytes_out,
NetworkErrorsIn: int64N(instance.Network.ErrorsIn), network_packets_in = EXCLUDED.network_packets_in,
NetworkErrorsOut: int64N(instance.Network.ErrorsOut), network_packets_out = EXCLUDED.network_packets_out,
StartTime: timeN(instance.StartTime), start_time = EXCLUDED.start_time
LastSeen: timeN(instance.LastSeen), `
HealthStatus: strN(instance.Health.Status),
HealthLastCheck: timeN(instance.Health.LastCheck), _, err := s.db.ExecContext(ctx, query,
HealthCheckCount: sql.NullInt32{Int32: int32(instance.Health.CheckCount), Valid: true}, nodeID, timestamp, container.ContainerID, container.Name, container.State,
HealthFailureCount: sql.NullInt32{Int32: int32(instance.Health.FailureCount), Valid: true}, container.CPU, container.Memory, container.Network.BytesIn, container.Network.BytesOut,
}) container.Network.PacketsIn, container.Network.PacketsOut, container.StartTime,
)
return err
} }
func (s *PostgreSQLMetricsStorage) getInstanceMetrics(ctx context.Context, serviceID string, timestamp time.Time) ([]InstanceMetrics, error) { func (s *PostgreSQLMetricsStorage) storeInstanceMetrics(ctx context.Context, serviceID string, timestamp time.Time, instance InstanceMetrics) error {
rows, err := s.q.ListInstanceMetrics(ctx, sqlcdb.ListInstanceMetricsParams{ query := `
ServiceID: serviceID, INSERT INTO instance_metrics (
Timestamp: timestamp, service_id, timestamp, instance_id, node_id, status, cpu, memory,
}) network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
start_time, last_seen, health_status, health_last_check, health_check_count, health_failure_count
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
ON CONFLICT (service_id, timestamp, instance_id) DO UPDATE SET
node_id = EXCLUDED.node_id,
status = EXCLUDED.status,
cpu = EXCLUDED.cpu,
memory = EXCLUDED.memory,
network_bytes_in = EXCLUDED.network_bytes_in,
network_bytes_out = EXCLUDED.network_bytes_out,
network_packets_in = EXCLUDED.network_packets_in,
network_packets_out = EXCLUDED.network_packets_out,
network_connections_in = EXCLUDED.network_connections_in,
network_connections_out = EXCLUDED.network_connections_out,
network_errors_in = EXCLUDED.network_errors_in,
network_errors_out = EXCLUDED.network_errors_out,
start_time = EXCLUDED.start_time,
last_seen = EXCLUDED.last_seen,
health_status = EXCLUDED.health_status,
health_last_check = EXCLUDED.health_last_check,
health_check_count = EXCLUDED.health_check_count,
health_failure_count = EXCLUDED.health_failure_count
`
_, err := s.db.ExecContext(ctx, query,
serviceID, timestamp, instance.InstanceID, instance.NodeID, instance.Status,
instance.CPU, instance.Memory, instance.Network.BytesIn, instance.Network.BytesOut,
instance.Network.PacketsIn, instance.Network.PacketsOut, instance.Network.ConnectionsIn,
instance.Network.ConnectionsOut, instance.Network.ErrorsIn, instance.Network.ErrorsOut,
instance.StartTime, instance.LastSeen, instance.Health.Status, instance.Health.LastCheck,
instance.Health.CheckCount, instance.Health.FailureCount,
)
return err
}
func (s *PostgreSQLMetricsStorage) getContainerMetrics(ctx context.Context, nodeID string, timestamp time.Time) ([]ContainerMetrics, error) {
query := `
SELECT container_id, name, state, cpu, memory,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out, start_time
FROM container_metrics
WHERE node_id = $1 AND timestamp = $2
`
rows, err := s.db.QueryContext(ctx, query, nodeID, timestamp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
var containers []ContainerMetrics
for rows.Next() {
var c ContainerMetrics
err := rows.Scan(
&c.ContainerID, &c.Name, &c.State, &c.CPU, &c.Memory,
&c.Network.BytesIn, &c.Network.BytesOut, &c.Network.PacketsIn, &c.Network.PacketsOut, &c.StartTime,
)
if err != nil {
return nil, err
}
containers = append(containers, c)
}
return containers, nil
}
func (s *PostgreSQLMetricsStorage) getInstanceMetrics(ctx context.Context, serviceID string, timestamp time.Time) ([]InstanceMetrics, error) {
query := `
SELECT instance_id, node_id, status, cpu, memory,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
start_time, last_seen, health_status, health_last_check, health_check_count, health_failure_count
FROM instance_metrics
WHERE service_id = $1 AND timestamp = $2
`
rows, err := s.db.QueryContext(ctx, query, serviceID, timestamp)
if err != nil {
return nil, err
}
defer rows.Close()
var instances []InstanceMetrics var instances []InstanceMetrics
for _, r := range rows { for rows.Next() {
instances = append(instances, InstanceMetrics{ var i InstanceMetrics
InstanceID: r.InstanceID, err := rows.Scan(
NodeID: r.NodeID.String, &i.InstanceID, &i.NodeID, &i.Status, &i.CPU, &i.Memory,
Status: r.Status.String, &i.Network.BytesIn, &i.Network.BytesOut, &i.Network.PacketsIn, &i.Network.PacketsOut,
CPU: f64v(r.Cpu), &i.Network.ConnectionsIn, &i.Network.ConnectionsOut, &i.Network.ErrorsIn, &i.Network.ErrorsOut,
Memory: i64v(r.Memory), &i.StartTime, &i.LastSeen, &i.Health.Status, &i.Health.LastCheck, &i.Health.CheckCount, &i.Health.FailureCount,
Network: NetworkMetrics{ )
BytesIn: i64v(r.NetworkBytesIn), if err != nil {
BytesOut: i64v(r.NetworkBytesOut), return nil, err
PacketsIn: i64v(r.NetworkPacketsIn), }
PacketsOut: i64v(r.NetworkPacketsOut), instances = append(instances, i)
ConnectionsIn: i32v(r.NetworkConnectionsIn),
ConnectionsOut: i32v(r.NetworkConnectionsOut),
ErrorsIn: i64v(r.NetworkErrorsIn),
ErrorsOut: i64v(r.NetworkErrorsOut),
},
StartTime: r.StartTime.Time,
LastSeen: r.LastSeen.Time,
Health: HealthMetrics{
Status: r.HealthStatus.String,
LastCheck: r.HealthLastCheck.Time,
CheckCount: int(r.HealthCheckCount.Int32),
FailureCount: int(r.HealthFailureCount.Int32),
},
})
} }
return instances, nil return instances, nil
} }
@@ -469,9 +544,7 @@ func (s *InMemoryMetricsStorage) GetServiceMetrics(ctx context.Context, serviceI
} }
func (s *InMemoryMetricsStorage) GetAggregatedMetrics(ctx context.Context, query MetricsQuery) (*AggregatedMetrics, error) { func (s *InMemoryMetricsStorage) GetAggregatedMetrics(ctx context.Context, query MetricsQuery) (*AggregatedMetrics, error) {
s.mu.RLock() // Simplified implementation
defer s.mu.RUnlock()
return &AggregatedMetrics{ return &AggregatedMetrics{
Query: query, Query: query,
TimeSeries: []TimeSeriesPoint{}, TimeSeries: []TimeSeriesPoint{},
@@ -1,130 +0,0 @@
package metrics
import (
"context"
"database/sql"
"os"
"testing"
"time"
)
// Live round-trip against a real Postgres — set METRICS_TEST_DSN (or
// DATABASE_URL) to enable. Skipped by default so unit runs stay hermetic.
func testDSN(t *testing.T) *sql.DB {
t.Helper()
dsn := os.Getenv("METRICS_TEST_DSN")
if dsn == "" {
dsn = os.Getenv("DATABASE_URL")
}
if dsn == "" {
t.Skip("METRICS_TEST_DSN not set")
}
db, err := sql.Open("postgres", dsn)
if err != nil {
t.Fatalf("open: %v", err)
}
if err := db.PingContext(context.Background()); err != nil {
t.Fatalf("ping: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}
func TestPostgresStorageRoundTrip(t *testing.T) {
db := testDSN(t)
s := NewPostgreSQLMetricsStorage(db)
ctx := context.Background()
ts := time.Now().UTC().Truncate(time.Second)
nodeID := "test-node-" + ts.Format("150405")
svcID := "test-svc-" + ts.Format("150405")
// Clean slate + teardown
db.ExecContext(ctx, `DELETE FROM instance_metrics WHERE service_id = $1`, svcID)
db.ExecContext(ctx, `DELETE FROM service_metrics WHERE service_id = $1`, svcID)
db.ExecContext(ctx, `DELETE FROM node_metrics WHERE node_id = $1`, nodeID)
t.Cleanup(func() {
db.ExecContext(ctx, `DELETE FROM instance_metrics WHERE service_id = $1`, svcID)
db.ExecContext(ctx, `DELETE FROM service_metrics WHERE service_id = $1`, svcID)
db.ExecContext(ctx, `DELETE FROM node_metrics WHERE node_id = $1`, nodeID)
})
node := &NodeMetrics{
NodeID: nodeID,
Timestamp: ts,
CPU: CPUMetrics{UsagePercent: 42.5, UsageCores: 1.7, LoadAverage1: 0.5, LoadAverage5: 0.4, LoadAverage15: 0.3},
Memory: MemoryMetrics{Total: 16 << 30, Used: 8 << 30, Available: 8 << 30, UsagePercent: 50.0},
Storage: StorageMetrics{Total: 100 << 30, Used: 40 << 30, Available: 60 << 30, UsagePercent: 40.0},
Network: NetworkMetrics{BytesIn: 1000, BytesOut: 2000, PacketsIn: 10, PacketsOut: 20, ConnectionsIn: 3, ConnectionsOut: 4, ErrorsIn: 0, ErrorsOut: 1},
System: SystemMetrics{Uptime: 3600 * time.Second, Processes: 120, OS: "linux", Kernel: "6.8", Architecture: "amd64"},
}
if err := s.StoreNodeMetrics(ctx, node); err != nil {
t.Fatalf("store node: %v", err)
}
// Upsert idempotent on the same key.
if err := s.StoreNodeMetrics(ctx, node); err != nil {
t.Fatalf("store node (repeat): %v", err)
}
svc := &ServiceMetrics{
ServiceID: svcID,
ServiceName: "svc",
ProjectID: "proj",
Timestamp: ts,
Requests: RequestMetrics{Total: 100, Success: 95, Errors: 5, AvgLatency: 12.5, P95Latency: 80, P99Latency: 120, Throughput: 3.3},
Errors: ErrorMetrics{Total: 5, Rate: 0.05},
Performance: PerformanceMetrics{ResponseTime: 12.5, Throughput: 3.3, Concurrency: 2, Saturation: 0.4, Utilization: 0.55},
Resources: ResourceMetrics{CPUUsage: 42.5, MemoryUsage: 512 << 20, StorageUsage: 1 << 30, NetworkUsage: 3000, ResourceScore: 0.7},
Instances: []InstanceMetrics{{
InstanceID: "inst-1",
NodeID: nodeID,
Status: "running",
CPU: 42.5,
Memory: 512 << 20,
Network: NetworkMetrics{BytesIn: 1000, BytesOut: 2000},
StartTime: ts.Add(-time.Hour),
LastSeen: ts,
Health: HealthMetrics{Status: "healthy", LastCheck: ts, CheckCount: 10, FailureCount: 0},
}},
}
if err := s.StoreServiceMetrics(ctx, svc); err != nil {
t.Fatalf("store service: %v", err)
}
from, to := ts.Add(-time.Minute), ts.Add(time.Minute)
gotNodes, err := s.GetNodeMetrics(ctx, nodeID, from, to)
if err != nil {
t.Fatalf("get nodes: %v", err)
}
if len(gotNodes) != 1 {
t.Fatalf("want 1 node row, got %d", len(gotNodes))
}
if got := gotNodes[0]; got.CPU.UsagePercent != 42.5 || got.System.Processes != 120 || got.System.Uptime != time.Hour {
t.Errorf("node round-trip mismatch: %+v", got)
}
gotSvcs, err := s.GetServiceMetrics(ctx, svcID, from, to)
if err != nil {
t.Fatalf("get services: %v", err)
}
if len(gotSvcs) != 1 {
t.Fatalf("want 1 service row, got %d", len(gotSvcs))
}
if gotSvcs[0].Requests.P99Latency != 120 {
t.Errorf("p99 mismatch: %v", gotSvcs[0].Requests.P99Latency)
}
if len(gotSvcs[0].Instances) != 1 || gotSvcs[0].Instances[0].Health.Status != "healthy" {
t.Errorf("instance join mismatch: %+v", gotSvcs[0].Instances)
}
agg, err := s.GetAggregatedMetrics(ctx, MetricsQuery{Type: "node", ID: nodeID, From: from, To: to, Interval: 5 * time.Minute})
if err != nil {
t.Fatalf("aggregate: %v", err)
}
if len(agg.TimeSeries) != 1 || agg.TimeSeries[0].Values["cpu_usage"] != 42.5 {
t.Errorf("aggregate mismatch: %+v", agg.TimeSeries)
}
if agg.Summary["cpu_usage"].Avg != 42.5 {
t.Errorf("summary mismatch: %+v", agg.Summary)
}
}
-37
View File
@@ -1,37 +0,0 @@
-- name: GetUserByEmailForAuth :one
SELECT id, email, password_hash, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE email = $1;
-- name: CountUsers :one
SELECT COUNT(*) FROM users;
-- name: CountUsersByEmail :one
SELECT COUNT(*) FROM users WHERE email = $1;
-- name: UpsertLocalUser :one
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
VALUES ($1, $2, $3, NULLIF($4::varchar, ''), $5)
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name,
avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = NOW()
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at;
-- name: InsertUserAdmin :one
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, $4)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at;
-- name: InsertUser :one
INSERT INTO users (email, password_hash, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at;
-- name: GetUserByID :one
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE id = $1;
-- name: UpdateUserProfile :exec
UPDATE users
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
WHERE id = $3;
-58
View File
@@ -1,58 +0,0 @@
-- name: GetCronJobProjectID :one
SELECT project_id FROM cron_jobs WHERE id = $1;
-- name: GetCronOwnerViaService :one
SELECT p.owner_id FROM projects p
JOIN services s ON s.project_id = p.id
WHERE s.id = $1;
-- name: InsertCronJob :exec
INSERT INTO cron_jobs (id, project_id, service_id, name, schedule, command, timezone, enabled, next_run_at, retention, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);
-- name: GetCronJob :one
SELECT id, project_id, service_id, name, schedule, command, timezone,
enabled, last_run_at, next_run_at, last_status, last_output,
retention, created_at, updated_at
FROM cron_jobs WHERE id = $1;
-- name: GetCronJobOwner :one
SELECT p.owner_id FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1;
-- name: DeleteCronJob :exec
DELETE FROM cron_jobs WHERE id = $1;
-- name: ListCronExecutions :many
SELECT id, cron_job_id, started_at, finished_at, status, output, error
FROM cron_executions
WHERE cron_job_id = $1
ORDER BY started_at DESC
LIMIT 100;
-- name: GetCronJobForTrigger :one
SELECT cj.service_id, cj.command, cj.schedule, cj.timezone, cj.retention, p.owner_id
FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1;
-- name: InsertCronExecution :exec
INSERT INTO cron_executions (id, cron_job_id, started_at, status)
VALUES ($1, $2, $3, $4);
-- name: ListDueCronJobs :many
SELECT id, project_id, service_id, name, schedule, command, timezone, enabled, retention
FROM cron_jobs
WHERE enabled = TRUE AND next_run_at IS NOT NULL AND next_run_at <= NOW();
-- name: FinishCronExecution :exec
UPDATE cron_executions SET finished_at = $1, status = $2, output = $3, error = $4 WHERE id = $5;
-- name: UpdateCronJobRun :exec
UPDATE cron_jobs SET last_run_at = $1, last_status = $2, last_output = $3, next_run_at = $4 WHERE id = $5;
-- name: TrimCronExecutions :exec
DELETE FROM cron_executions WHERE cron_executions.cron_job_id = $1 AND cron_executions.id NOT IN (
SELECT ce.id FROM cron_executions ce WHERE ce.cron_job_id = $1
ORDER BY ce.started_at DESC LIMIT $2);
-105
View File
@@ -1,105 +0,0 @@
-- 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;
-46
View File
@@ -1,46 +0,0 @@
-- name: ListServiceDomains :many
SELECT id, service_id, domain, is_default, cert_type, cert_status, last_checked_at, created_at
FROM service_domains
WHERE service_id = $1
ORDER BY is_default DESC, created_at ASC;
-- name: CountServiceDomains :one
SELECT COUNT(*) FROM service_domains WHERE service_id = $1;
-- name: ClearServiceDomainDefault :exec
UPDATE service_domains SET is_default = false WHERE service_id = $1;
-- name: CreateServiceDomain :one
INSERT INTO service_domains (service_id, domain, is_default)
VALUES ($1, $2, $3)
RETURNING id, service_id, domain, is_default, cert_type, cert_status, created_at;
-- name: DeleteServiceDomain :execrows
DELETE FROM service_domains WHERE id = $1 AND service_id = $2;
-- name: SetServiceDomainDefault :execrows
UPDATE service_domains SET is_default = (id = $1) WHERE service_id = $2;
-- name: TouchServiceDomainChecked :exec
UPDATE service_domains SET last_checked_at = NOW() WHERE id = $1;
-- name: GetServiceDomainDefault :one
SELECT domain FROM service_domains WHERE service_id = $1 AND is_default;
-- name: GetServiceDomainFirst :one
SELECT domain FROM service_domains WHERE service_id = $1 ORDER BY created_at ASC LIMIT 1;
-- name: SetServiceLegacyDomain :exec
UPDATE services SET domain = $1 WHERE id = $2;
-- name: GetServicePlacementBrief :one
SELECT name, node_id, COALESCE(spread, false) AS spread FROM services WHERE id = $1;
-- name: GetServiceAccess :one
SELECT maintenance_mode, basic_auth_users FROM services WHERE id = $1;
-- name: ServiceWriteAccess :one
SELECT EXISTS(
SELECT 1 FROM services s JOIN projects p ON s.project_id = p.id
WHERE s.id = $1 AND (p.owner_id = $2 OR $3::bool)
) AS allowed;
-129
View File
@@ -1,129 +0,0 @@
-- name: ListGitProviders :many
SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at
FROM git_providers
WHERE user_id = $1
ORDER BY created_at DESC;
-- name: UpsertGitHubAppProvider :one
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
ON CONFLICT (name, user_id)
DO UPDATE SET
display_name = EXCLUDED.display_name,
api_url = EXCLUDED.api_url,
webhook_url = EXCLUDED.webhook_url,
access_token = EXCLUDED.access_token,
updated_at = NOW()
RETURNING id, created_at, updated_at;
-- name: InsertGitProvider :exec
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW());
-- name: GetGitProviderTimestamps :one
SELECT created_at, updated_at FROM git_providers WHERE id = $1;
-- name: GetGitProviderForRepos :one
SELECT name, display_name, access_token, api_url
FROM git_providers
WHERE id = $1 AND user_id = $2;
-- name: GetGitProviderName :one
SELECT name FROM git_providers WHERE id = $1 AND user_id = $2;
-- name: GetGitProviderForFetch :one
SELECT name, api_url, access_token
FROM git_providers
WHERE id = $1 AND user_id = $2;
-- name: DeleteGitProvider :execrows
DELETE FROM git_providers WHERE id = $1 AND user_id = $2;
-- name: ListGitRepositories :many
SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description,
clone_url, COALESCE(webhook_url, '') AS webhook_url,
default_branch, is_private, user_id, created_at, updated_at
FROM git_repositories
WHERE provider_id = $1 AND user_id = $2
AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'))
ORDER BY updated_at DESC
LIMIT $4 OFFSET $5;
-- name: CountGitRepositories :one
SELECT COUNT(*)
FROM git_repositories
WHERE provider_id = $1 AND user_id = $2
AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'));
-- name: GetGitRepositoryByFullName :one
SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description,
clone_url, COALESCE(webhook_url, '') AS webhook_url,
default_branch, is_private, user_id, created_at, updated_at
FROM git_repositories
WHERE provider_id = $1 AND full_name = $2 AND user_id = $3;
-- name: InsertGitRepository :one
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
RETURNING created_at, updated_at;
-- name: UpsertGitRepository :exec
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
ON CONFLICT (provider_id, full_name)
DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
clone_url = EXCLUDED.clone_url,
default_branch = EXCLUDED.default_branch,
is_private = EXCLUDED.is_private,
updated_at = NOW();
-- name: GetGitRepositoryProviderID :one
SELECT provider_id FROM git_repositories WHERE id = $1 AND user_id = $2;
-- name: UpsertGitWebhook :one
INSERT INTO git_webhooks (id, repo_id, provider_id, events, webhook_secret, active, branch_filter, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, TRUE, $6, NOW(), NOW())
ON CONFLICT (repo_id, provider_id)
DO UPDATE SET
events = EXCLUDED.events,
webhook_secret = EXCLUDED.webhook_secret,
active = TRUE,
branch_filter = EXCLUDED.branch_filter,
updated_at = NOW()
RETURNING id, active, created_at, updated_at;
-- name: ListConnectedRepositories :many
SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.description, '') AS description, r.clone_url,
r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at,
p.name AS provider_name, p.display_name
FROM git_repositories r
JOIN git_providers p ON r.provider_id = p.id
WHERE r.user_id = $1
ORDER BY r.updated_at DESC
LIMIT $2 OFFSET $3;
-- name: CountUserRepositories :one
SELECT COUNT(*) FROM git_repositories WHERE user_id = $1;
-- name: GetGitWebhookForPush :one
SELECT webhook_secret, repo_id, provider_id, COALESCE(branch_filter, '') AS branch_filter, active
FROM git_webhooks WHERE id = $1;
-- name: GetGitProviderNameByID :one
SELECT name FROM git_providers WHERE id = $1;
-- name: GetGitRepoForPush :one
SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1;
-- name: ListServicesForPush :many
SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status,
COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command,
COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo,
COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path,
COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS memory,
s.created_at, s.updated_at
FROM services s
WHERE (s.git_branch = $1 OR $1 = '') AND (s.git_repo = $2 OR s.git_repo = $3);
-147
View File
@@ -1,147 +0,0 @@
-- name: UpsertNodeMetrics :exec
INSERT INTO node_metrics (
node_id, timestamp, cpu_usage, cpu_cores, load_avg_1, load_avg_5, load_avg_15,
memory_total, memory_used, memory_available, memory_usage_percent,
storage_total, storage_used, storage_available, storage_usage_percent,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
uptime, processes, os, kernel, architecture
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28
)
ON CONFLICT (node_id, timestamp) DO UPDATE SET
cpu_usage = EXCLUDED.cpu_usage,
cpu_cores = EXCLUDED.cpu_cores,
load_avg_1 = EXCLUDED.load_avg_1,
load_avg_5 = EXCLUDED.load_avg_5,
load_avg_15 = EXCLUDED.load_avg_15,
memory_total = EXCLUDED.memory_total,
memory_used = EXCLUDED.memory_used,
memory_available = EXCLUDED.memory_available,
memory_usage_percent = EXCLUDED.memory_usage_percent,
storage_total = EXCLUDED.storage_total,
storage_used = EXCLUDED.storage_used,
storage_available = EXCLUDED.storage_available,
storage_usage_percent = EXCLUDED.storage_usage_percent,
network_bytes_in = EXCLUDED.network_bytes_in,
network_bytes_out = EXCLUDED.network_bytes_out,
network_packets_in = EXCLUDED.network_packets_in,
network_packets_out = EXCLUDED.network_packets_out,
network_connections_in = EXCLUDED.network_connections_in,
network_connections_out = EXCLUDED.network_connections_out,
network_errors_in = EXCLUDED.network_errors_in,
network_errors_out = EXCLUDED.network_errors_out,
uptime = EXCLUDED.uptime,
processes = EXCLUDED.processes,
os = EXCLUDED.os,
kernel = EXCLUDED.kernel,
architecture = EXCLUDED.architecture;
-- name: UpsertServiceMetrics :exec
INSERT INTO service_metrics (
service_id, service_name, project_id, timestamp,
requests_total, requests_success, requests_errors, requests_avg_latency,
requests_p95_latency, requests_p99_latency, requests_throughput,
errors_total, errors_rate, performance_response_time, performance_throughput,
performance_concurrency, performance_saturation, performance_utilization,
resource_cpu_usage, resource_memory_usage, resource_storage_usage,
resource_network_usage, resource_score
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21, $22, $23
)
ON CONFLICT (service_id, timestamp) DO UPDATE SET
requests_total = EXCLUDED.requests_total,
requests_success = EXCLUDED.requests_success,
requests_errors = EXCLUDED.requests_errors,
requests_avg_latency = EXCLUDED.requests_avg_latency,
requests_p95_latency = EXCLUDED.requests_p95_latency,
requests_p99_latency = EXCLUDED.requests_p99_latency,
requests_throughput = EXCLUDED.requests_throughput,
errors_total = EXCLUDED.errors_total,
errors_rate = EXCLUDED.errors_rate,
performance_response_time = EXCLUDED.performance_response_time,
performance_throughput = EXCLUDED.performance_throughput,
performance_concurrency = EXCLUDED.performance_concurrency,
performance_saturation = EXCLUDED.performance_saturation,
performance_utilization = EXCLUDED.performance_utilization,
resource_cpu_usage = EXCLUDED.resource_cpu_usage,
resource_memory_usage = EXCLUDED.resource_memory_usage,
resource_storage_usage = EXCLUDED.resource_storage_usage,
resource_network_usage = EXCLUDED.resource_network_usage,
resource_score = EXCLUDED.resource_score;
-- name: UpsertInstanceMetrics :exec
INSERT INTO instance_metrics (
service_id, timestamp, instance_id, node_id, status, cpu, memory,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
start_time, last_seen, health_status, health_last_check, health_check_count, health_failure_count
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21
)
ON CONFLICT (service_id, timestamp, instance_id) DO UPDATE SET
node_id = EXCLUDED.node_id,
status = EXCLUDED.status,
cpu = EXCLUDED.cpu,
memory = EXCLUDED.memory,
network_bytes_in = EXCLUDED.network_bytes_in,
network_bytes_out = EXCLUDED.network_bytes_out,
network_packets_in = EXCLUDED.network_packets_in,
network_packets_out = EXCLUDED.network_packets_out,
network_connections_in = EXCLUDED.network_connections_in,
network_connections_out = EXCLUDED.network_connections_out,
network_errors_in = EXCLUDED.network_errors_in,
network_errors_out = EXCLUDED.network_errors_out,
start_time = EXCLUDED.start_time,
last_seen = EXCLUDED.last_seen,
health_status = EXCLUDED.health_status,
health_last_check = EXCLUDED.health_last_check,
health_check_count = EXCLUDED.health_check_count,
health_failure_count = EXCLUDED.health_failure_count;
-- name: ListNodeMetrics :many
SELECT node_id, timestamp, cpu_usage, cpu_cores, load_avg_1, load_avg_5, load_avg_15,
memory_total, memory_used, memory_available, memory_usage_percent,
storage_total, storage_used, storage_available, storage_usage_percent,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
coalesce(extract(epoch from uptime), 0)::bigint AS uptime_seconds,
processes, os, kernel, architecture
FROM node_metrics
WHERE node_id = $1 AND timestamp BETWEEN $2 AND $3
ORDER BY timestamp ASC;
-- name: ListServiceMetrics :many
SELECT service_id, service_name, project_id, timestamp,
requests_total, requests_success, requests_errors, requests_avg_latency,
requests_p95_latency, requests_p99_latency, requests_throughput,
errors_total, errors_rate, performance_response_time, performance_throughput,
performance_concurrency, performance_saturation, performance_utilization,
resource_cpu_usage, resource_memory_usage, resource_storage_usage,
resource_network_usage, resource_score
FROM service_metrics
WHERE service_id = $1 AND timestamp BETWEEN $2 AND $3
ORDER BY timestamp ASC;
-- name: ListInstanceMetrics :many
SELECT instance_id, node_id, status, cpu, memory,
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
start_time, last_seen, health_status, health_last_check, health_check_count, health_failure_count
FROM instance_metrics
WHERE service_id = $1 AND timestamp = $2;
-- name: AggregateNodeMetrics :many
-- Portable bucket bucketing (no TimescaleDB): floor epoch seconds into
-- bucket-sized buckets, then back to timestamptz.
SELECT date_bin($1::interval, timestamp, '2000-01-01'::timestamptz) AS bucket,
AVG(cpu_usage)::float8 AS avg_cpu,
AVG(memory_usage_percent)::float8 AS avg_memory,
AVG(storage_usage_percent)::float8 AS avg_storage
FROM node_metrics
WHERE node_id = $2 AND timestamp BETWEEN $3 AND $4
GROUP BY bucket
ORDER BY bucket ASC;
@@ -1,121 +0,0 @@
-- 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;
-59
View File
@@ -1,59 +0,0 @@
-- name: ListNodeAgentsForService :many
SELECT DISTINCT node_agent_id FROM container_instances WHERE service_id = $1;
-- name: ListServiceVariableValues :many
SELECT key, value FROM environment_variables WHERE service_id = $1;
-- name: ListServiceVariablesWithSecret :many
SELECT key, value, COALESCE(is_secret, false) AS is_secret
FROM environment_variables WHERE service_id = $1;
-- name: ListProjectServiceRefs :many
SELECT id, name FROM services WHERE project_id = $1;
-- name: GetServicePort :one
SELECT COALESCE(port, 0) AS port FROM services WHERE id = $1;
-- name: GetServiceVariableValue :one
SELECT value FROM environment_variables WHERE service_id = $1 AND key = $2;
-- name: GetServiceRuntimeWithOwner :one
SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status,
COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command,
COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo,
COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path,
COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS 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: GetLastDeployedImage :one
SELECT (image_name || ':' || image_tag)::text AS image FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1;
-- name: SetServicePublishedPort :exec
UPDATE services SET published_port = $1 WHERE id = $2 AND published_port <> $1;
-- name: SetServiceReplicas :exec
UPDATE services SET replicas = $1, updated_at = $2 WHERE id = $3;
-- name: ListSleepCandidates :many
SELECT id, project_id, name, COALESCE(status,'') AS status,
COALESCE(sleep_idle_minutes, 15) AS sleep_idle_minutes
FROM services
WHERE sleep_enabled AND COALESCE(status,'') IN ('running','deployed','degraded','starting');
-- name: GetServiceDomainText :one
SELECT COALESCE(domain,'') AS domain FROM services WHERE id = $1;
-- name: GetServiceStatusText :one
SELECT COALESCE(status,'') AS status FROM services WHERE id = $1;
-78
View File
@@ -1,78 +0,0 @@
-- name: GetProjectOwnerText :one
SELECT owner_id::text FROM projects WHERE id = $1;
-- name: ServiceExistsInProject :one
SELECT EXISTS(SELECT 1 FROM services WHERE id = $1 AND project_id = $2);
-- name: ListVulnerabilities :many
SELECT id, type, severity, title, description, service_id, status, found_at, resolved_at
FROM vulnerabilities
WHERE project_id = $1
ORDER BY
CASE severity
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
found_at DESC;
-- name: UpdateVulnerabilityStatus :exec
UPDATE vulnerabilities SET status = $1, resolved_at = $2 WHERE id = $3;
-- name: ComplianceFrameworkExists :one
SELECT EXISTS(SELECT 1 FROM compliance_frameworks WHERE id = $1);
-- name: ListComplianceFrameworks :many
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name;
-- name: GetVulnerabilityMetrics :one
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE severity = 'critical') AS critical,
COUNT(*) FILTER (WHERE severity = 'high') AS high,
COUNT(*) FILTER (WHERE severity = 'medium') AS medium,
COUNT(*) FILTER (WHERE severity = 'low') AS low,
COUNT(*) FILTER (WHERE status = 'open') AS open,
COUNT(*) FILTER (WHERE status = 'resolved') AS resolved
FROM vulnerabilities
WHERE project_id = $1;
-- name: GetLatestSecurityScan :one
SELECT id, COALESCE((summary->>'score')::int, 0)::int AS score, started_at AS scanned_at, status
FROM security_scans
WHERE project_id = $1
ORDER BY started_at DESC
LIMIT 1;
-- name: GetLatestComplianceReport :one
SELECT overall_status, score, assessment_date
FROM compliance_reports
WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1;
-- name: CheckSecurityProjectAccess :one
SELECT EXISTS (
SELECT 1
FROM projects p
WHERE p.id = $1
AND (p.is_approved
OR $3::bool
OR p.owner_id = $2 OR EXISTS (
SELECT 1 FROM project_members pm
WHERE pm.project_id = p.id AND pm.user_id = $2
))
);
-- name: GetSecurityScanProjectID :one
SELECT project_id FROM security_scans WHERE id = $1;
-- name: GetComplianceReportProjectID :one
SELECT project_id FROM compliance_reports WHERE id = $1;
-- name: GetVulnerabilityProjectID :one
SELECT project_id FROM vulnerabilities WHERE id = $1;
-38
View File
@@ -1,38 +0,0 @@
-- 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;
+3 -247
View File
@@ -69,8 +69,6 @@ CREATE TABLE services (
static_dir VARCHAR(255) NOT NULL DEFAULT '', static_dir VARCHAR(255) NOT NULL DEFAULT '',
node_id VARCHAR(255), node_id VARCHAR(255),
spread BOOLEAN NOT NULL DEFAULT false, spread BOOLEAN NOT NULL DEFAULT false,
sleep_enabled BOOLEAN NOT NULL DEFAULT false,
sleep_idle_minutes INTEGER NOT NULL DEFAULT 15,
placement_tags JSONB NOT NULL DEFAULT '[]', placement_tags JSONB NOT NULL DEFAULT '[]',
traefik_labels JSONB NOT NULL DEFAULT '{}' traefik_labels JSONB NOT NULL DEFAULT '{}'
); );
@@ -100,37 +98,11 @@ CREATE TABLE service_domains (
); );
CREATE TABLE deployments ( CREATE TABLE deployments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY,
service_id UUID NOT NULL REFERENCES services(id) ON DELETE CASCADE, service_id UUID NOT NULL REFERENCES services(id) ON DELETE CASCADE,
version VARCHAR(255) NOT NULL, status VARCHAR(50),
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 (
@@ -374,219 +346,3 @@ CREATE TABLE notification_channels (
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()
); );
CREATE TABLE node_metrics (
node_id VARCHAR(255) NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
cpu_usage NUMERIC(5,2),
cpu_cores NUMERIC(10,2),
load_avg_1 NUMERIC(5,2),
load_avg_5 NUMERIC(5,2),
load_avg_15 NUMERIC(5,2),
memory_total BIGINT,
memory_used BIGINT,
memory_available BIGINT,
memory_usage_percent NUMERIC(5,2),
storage_total BIGINT,
storage_used BIGINT,
storage_available BIGINT,
storage_usage_percent NUMERIC(5,2),
network_bytes_in BIGINT,
network_bytes_out BIGINT,
network_packets_in BIGINT,
network_packets_out BIGINT,
network_connections_in INTEGER,
network_connections_out INTEGER,
network_errors_in BIGINT,
network_errors_out BIGINT,
uptime INTERVAL,
processes INTEGER,
os VARCHAR(50),
kernel VARCHAR(50),
architecture VARCHAR(20),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (node_id, timestamp)
);
CREATE TABLE service_metrics (
service_id VARCHAR(255) NOT NULL,
service_name VARCHAR(255) NOT NULL,
project_id VARCHAR(255) NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
requests_total BIGINT DEFAULT 0,
requests_success BIGINT DEFAULT 0,
requests_errors BIGINT DEFAULT 0,
requests_avg_latency NUMERIC(10,3),
requests_p95_latency NUMERIC(10,3),
requests_p99_latency NUMERIC(10,3),
requests_throughput NUMERIC(10,3),
errors_total BIGINT DEFAULT 0,
errors_rate NUMERIC(5,4),
performance_response_time NUMERIC(10,3),
performance_throughput NUMERIC(10,3),
performance_concurrency BIGINT,
performance_saturation NUMERIC(5,2),
performance_utilization NUMERIC(5,2),
resource_cpu_usage NUMERIC(5,2),
resource_memory_usage BIGINT,
resource_storage_usage BIGINT,
resource_network_usage BIGINT,
resource_score NUMERIC(5,2),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (service_id, timestamp)
);
CREATE TABLE instance_metrics (
service_id VARCHAR(255) NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
instance_id VARCHAR(255) NOT NULL,
node_id VARCHAR(255),
status VARCHAR(50),
cpu NUMERIC(5,2),
memory BIGINT,
network_bytes_in BIGINT,
network_bytes_out BIGINT,
network_packets_in BIGINT,
network_packets_out BIGINT,
network_connections_in INTEGER,
network_connections_out INTEGER,
network_errors_in BIGINT,
network_errors_out BIGINT,
start_time TIMESTAMP WITH TIME ZONE,
last_seen TIMESTAMP WITH TIME ZONE,
health_status VARCHAR(20),
health_last_check TIMESTAMP WITH TIME ZONE,
health_check_count INTEGER DEFAULT 0,
health_failure_count INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (service_id, timestamp, instance_id)
);
CREATE TABLE cron_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
service_id UUID NOT NULL REFERENCES services(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
schedule VARCHAR(100) NOT NULL,
command TEXT NOT NULL,
timezone VARCHAR(50) DEFAULT 'UTC',
enabled BOOLEAN DEFAULT true,
last_run_at TIMESTAMP WITH TIME ZONE,
next_run_at TIMESTAMP WITH TIME ZONE,
last_status VARCHAR(50),
last_output TEXT,
retention INTEGER DEFAULT 30,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE cron_executions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cron_job_id UUID NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
started_at TIMESTAMP WITH TIME ZONE NOT NULL,
finished_at TIMESTAMP WITH TIME ZONE,
status VARCHAR(50) DEFAULT 'pending',
output TEXT,
error TEXT
);
CREATE TABLE git_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
display_name VARCHAR(255) NOT NULL,
api_url VARCHAR(255) NOT NULL,
webhook_url VARCHAR(255) NOT NULL,
access_token TEXT NOT NULL,
user_id UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE git_repositories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
full_name VARCHAR(255) NOT NULL,
description TEXT,
clone_url VARCHAR(255) NOT NULL,
webhook_url VARCHAR(255),
default_branch VARCHAR(255) DEFAULT 'main',
is_private BOOLEAN DEFAULT false,
user_id UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE git_webhooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
repo_id UUID NOT NULL,
provider_id UUID NOT NULL,
events TEXT NOT NULL,
webhook_secret TEXT NOT NULL,
remote_webhook_id VARCHAR(255),
active BOOLEAN DEFAULT true,
branch_filter VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
avatar_url VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_admin BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE compliance_frameworks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
version VARCHAR(20) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE security_scans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL,
service_id UUID,
scan_type VARCHAR(50) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'running',
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
summary JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE compliance_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL,
framework_id UUID NOT NULL,
assessment_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
assessor VARCHAR(255),
overall_status VARCHAR(50) NOT NULL,
score INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE vulnerabilities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(50) NOT NULL,
severity VARCHAR(20) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
service_id UUID,
project_id UUID NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'open',
found_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);