Compare commits

..
Author SHA1 Message Date
Tomas DvorakandDevin 8379f50c54 refactor(db): port preview environments to sqlc
Tranche 3: all 30 raw sites in preview_environments.go — list/detail
joins, access-check reads, create/update/delete, promote, user cleanup,
TTL sweep, and the status-sync UPDATE...FROM CASE. Deployments and
preview_environments schema.sql definitions completed to match runtime.
Shared mapper converts generated rows to the PreviewEnvironment DTO;
DeleteServiceByID from tranche 2 is reused for clone teardown.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-10 03:32:46 +02:00
22 changed files with 937 additions and 3919 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
service's own scope (one env per service); promote ships via preview
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** — ~300 raw `database/sql` sites → `sqlc/queries/`. Done in tranches (dynamic queries stay raw): metrics storage + service domains ported.
- [ ] **Tests** — canvas/wizard frontend coverage, deployment/template/
webhook table tests, Playwright e2e golden path.
- [x] **First-run wizard** — public `GET /setup/status` + authed
+122 -146
View File
@@ -65,47 +65,13 @@ type UpdateCronJobRequest struct {
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.
func cronJobProjectID(db *database.DB, jobID string) (uuid.UUID, bool) {
id, err := uuid.Parse(jobID)
if err != nil {
return uuid.Nil, false
}
projectID, err := sqlcdb.New(db.DB).GetCronJobProjectID(context.Background(), id)
var projectID uuid.UUID
err := db.QueryRow(
`SELECT cj.project_id FROM cron_jobs cj WHERE cj.id = $1`,
jobID,
).Scan(&projectID)
if err != nil {
return uuid.Nil, false
}
@@ -171,14 +137,15 @@ func handleCreateCronJob(c *gin.Context) {
return
}
svcID, err := uuid.Parse(req.ServiceID)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
ownerCheck, err := sqlcdb.New(db.DB).GetCronOwnerViaService(context.Background(), svcID)
var ownerCheck string
err := db.QueryRow(
`SELECT p.owner_id FROM projects p
JOIN services s ON s.project_id = p.id
WHERE s.id = $1`,
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"})
return
}
@@ -211,20 +178,11 @@ func handleCreateCronJob(c *gin.Context) {
UpdatedAt: time.Now(),
}
err = sqlcdb.New(db.DB).InsertCronJob(context.Background(), sqlcdb.InsertCronJobParams{
ID: uuid.MustParse(job.ID),
ProjectID: uuid.MustParse(job.ProjectID),
ServiceID: uuid.MustParse(job.ServiceID),
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},
})
_, err = db.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)`,
job.ID, job.ProjectID, job.ServiceID, job.Name, job.Schedule, job.Command, job.Timezone, job.Enabled, job.NextRunAt, job.Retention, job.CreatedAt, job.UpdatedAt,
)
if err != nil {
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)
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Cron job not found"})
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)
if !found {
@@ -279,14 +242,15 @@ func handleUpdateCronJob(c *gin.Context) {
return
}
jid, err := uuid.Parse(jobID)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
ownerCheck, err := sqlcdb.New(db.DB).GetCronJobOwner(context.Background(), jid)
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.String() != userID && !contextIsAdmin(c)) {
if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
@@ -348,19 +312,21 @@ func handleDeleteCronJob(c *gin.Context) {
db := c.MustGet("db").(*database.DB)
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 {
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"})
return
}
@@ -384,20 +350,29 @@ func handleGetCronExecutions(c *gin.Context) {
return
}
jid, err := uuid.Parse(jobID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"})
return
}
rows, err := sqlcdb.New(db.DB).ListCronExecutions(context.Background(), jid)
rows, err := db.Query(
`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`,
jobID,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"})
return
}
defer rows.Close()
var executions []CronExecution
for _, e := range rows {
executions = append(executions, cronExecutionFrom(e))
for rows.Next() {
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})
@@ -408,33 +383,29 @@ func handleTriggerCronJob(c *gin.Context) {
db := c.MustGet("db").(*database.DB)
jobID := c.Param("id")
jid, err := uuid.Parse(jobID)
if err != nil {
var job CronJob
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"})
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()
now := time.Now()
_ = sqlcdb.New(db.DB).InsertCronExecution(context.Background(), sqlcdb.InsertCronExecutionParams{
ID: uuid.MustParse(execID),
CronJobID: jid,
StartedAt: now,
Status: sql.NullString{String: "running", Valid: true},
})
_, err = db.Exec(
`INSERT INTO cron_executions (id, cron_job_id, started_at, status)
VALUES ($1, $2, $3, $4)`,
execID, jobID, now, "running",
)
dockerClient, _ := c.Get("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) {
q := sqlcdb.New(db.DB)
jobs, err := q.ListDueCronJobs(context.Background())
rows, err := db.Query(
`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 {
log.Printf("cron scheduler: failed to list due jobs: %v", err)
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 {
execID := uuid.New().String()
if err := q.InsertCronExecution(context.Background(), sqlcdb.InsertCronExecutionParams{
ID: uuid.MustParse(execID),
CronJobID: j.ID,
StartedAt: time.Now(),
Status: sql.NullString{String: "running", Valid: true},
}); err != nil {
if _, err := db.Exec(
`INSERT INTO cron_executions (id, cron_job_id, started_at, status) VALUES ($1, $2, $3, $4)`,
execID, j.ID, time.Now(), "running"); err != nil {
log.Printf("cron scheduler: failed to record execution for %s: %v", j.ID, err)
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()
q := sqlcdb.New(db.DB)
if eid, err := uuid.Parse(execID); err == nil {
_ = q.FinishCronExecution(context.Background(), sqlcdb.FinishCronExecutionParams{
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,
})
}
db.Exec(
`UPDATE cron_executions SET finished_at = $1, status = $2, output = $3, error = $4 WHERE id = $5`,
now, status, output, errText, execID,
)
next, _ := calculateNextRun(schedule, timezone)
if jid, err := uuid.Parse(jobID); err == nil {
_ = q.UpdateCronJobRun(context.Background(), sqlcdb.UpdateCronJobRunParams{
LastRunAt: sql.NullTime{Time: now, Valid: true},
LastStatus: sql.NullString{String: status, Valid: true},
LastOutput: sql.NullString{String: output, Valid: true},
NextRunAt: ntPtr(next),
ID: jid,
})
db.Exec(
`UPDATE cron_jobs SET last_run_at = $1, last_status = $2, last_output = $3, next_run_at = $4 WHERE id = $5`,
now, status, output, next, jobID,
)
if retention > 0 {
_ = q.TrimCronExecutions(context.Background(), sqlcdb.TrimCronExecutionsParams{
CronJobID: jid,
Limit: int32(retention),
})
}
db.Exec(
`DELETE FROM cron_executions WHERE cron_job_id = $1 AND id NOT IN (
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
import (
"context"
"database/sql"
"strings"
"testing"
"time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
)
func TestCalculateNextRun(t *testing.T) {
@@ -71,123 +64,3 @@ func TestCalculateNextRunTimezone(t *testing.T) {
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 (
"containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment"
"containr/internal/deployqueue"
"containr/internal/docker"
"containr/internal/source"
"containr/internal/types"
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
@@ -39,72 +37,6 @@ type DeploymentModel struct {
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 {
CommitHash string `json:"commit_hash"`
Branch string `json:"branch"`
@@ -152,29 +84,44 @@ func handleGetDeployments(c *gin.Context) {
offset, limit := pageWindow(c, 50, 100)
q := sqlcdb.New(db.(*database.DB).DB)
total, _ := q.CountDeploymentsForService(c.Request.Context(), serviceID)
var total int
_ = db.(*database.DB).QueryRow(
`SELECT COUNT(*) FROM deployments WHERE service_id = $1`, serviceID,
).Scan(&total)
rows, err := q.ListDeploymentsForService(c.Request.Context(), sqlcdb.ListDeploymentsForServiceParams{
ServiceID: serviceID, Limit: int32(limit), Offset: int32(offset)})
rows, err := db.(*database.DB).Query(
`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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"})
return
}
defer rows.Close()
deployments := make([]DeploymentModel, 0, len(rows))
for _, r := range rows {
deployments = append(deployments, 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,
}))
var deployments []DeploymentModel
for rows.Next() {
var d DeploymentModel
err := rows.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,
)
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{
"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)
q := sqlcdb.New(db.(*database.DB).DB)
total, err := q.CountAccessibleDeployments(c.Request.Context(), sqlcdb.CountAccessibleDeploymentsParams{
OwnerID: userID, Column2: isAdmin})
if err != nil {
var total int
if err := db.(*database.DB).QueryRow(
`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)`,
userID, isAdmin,
).Scan(&total); err != nil {
total = 0
}
rows, err := q.ListRecentAccessibleDeployments(c.Request.Context(), sqlcdb.ListRecentAccessibleDeploymentsParams{
OwnerID: userID, Column2: isAdmin, Limit: int32(limit), Offset: int32(offset)})
rows, err := db.(*database.DB).Query(
`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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"})
return
}
defer rows.Close()
deployments := make([]RecentDeployment, 0, len(rows))
for _, r := range rows {
deployments = append(deployments, RecentDeployment{
ID: r.ID, ServiceID: r.ServiceID, ServiceName: r.ServiceName,
ProjectName: r.ProjectName, Status: r.Status.String, ImageName: r.ImageName,
StartedAt: timePtrNT(r.StartedAt), CompletedAt: timePtrNT(r.CompletedAt),
CreatedAt: r.CreatedAt.Time,
})
deployments := []RecentDeployment{}
for rows.Next() {
var d RecentDeployment
if err := rows.Scan(&d.ID, &d.ServiceID, &d.ServiceName, &d.ProjectName,
&d.Status, &d.ImageName, &d.StartedAt, &d.CompletedAt, &d.CreatedAt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan deployment"})
return
}
deployments = append(deployments, d)
}
c.JSON(http.StatusOK, gin.H{
"deployments": deployments,
"pagination": paginationMeta(offset, limit, int(total)),
"pagination": paginationMeta(offset, limit, total),
})
}
@@ -265,27 +230,38 @@ func handleCreateDeployment(c *gin.Context) {
return
}
ctx := c.Request.Context()
q := sqlcdb.New(db.(*database.DB).DB)
sr, err := q.GetServiceForDeployWithOwner(ctx, serviceID)
var service Service
var projectOwner string
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
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"})
return
}
@@ -311,17 +287,12 @@ func handleCreateDeployment(c *gin.Context) {
UpdatedAt: now,
}
err = q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{
ID: d.ID,
ServiceID: d.ServiceID,
Version: fmt.Sprintf("v%d", now.Unix()),
CommitHash: sql.NullString{String: ptrStr(d.CommitHash), Valid: d.CommitHash != nil},
Status: sql.NullString{String: d.Status, Valid: true},
ImageName: sql.NullString{String: d.ImageName, Valid: true},
ImageTag: sql.NullString{String: d.ImageTag, Valid: true},
CreatedAt: sql.NullTime{Time: d.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: d.UpdatedAt, Valid: true},
})
_, err = db.(*database.DB).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)`,
d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create deployment"})
@@ -332,20 +303,20 @@ func handleCreateDeployment(c *gin.Context) {
if !exists || engine == nil {
unavailableErr := "Deployment engine unavailable. Docker may not be configured on this server."
completedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
Error: sql.NullString{String: unavailableErr, Valid: true},
CompletedAt: sql.NullTime{Time: completedAt, Valid: true},
ID: d.ID,
})
_, _ = db.(*database.DB).Exec(
`UPDATE deployments
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
WHERE id = $3`,
unavailableErr, completedAt, d.ID,
)
d.Status = "failed"
d.Error = &unavailableErr
d.CompletedAt = &completedAt
} else {
err = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "building", Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: serviceID,
})
_, err = db.(*database.DB).Exec(
`UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
time.Now(), serviceID,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service status"})
return
@@ -360,17 +331,14 @@ func handleCreateDeployment(c *gin.Context) {
})
if pos > 0 {
d.Status = "queued"
now := time.Now()
_ = q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{
Status: sql.NullString{String: "queued", Valid: true},
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: d.ID,
})
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "queued", Valid: true},
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: serviceID,
})
_, _ = db.(*database.DB).Exec(
`UPDATE deployments SET status = 'queued', updated_at = $1 WHERE id = $2`,
time.Now(), d.ID,
)
_, _ = db.(*database.DB).Exec(
`UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`,
time.Now(), serviceID,
)
}
}
@@ -412,7 +380,6 @@ func runDeploymentAndSyncWithImage(
) {
ctx, cancel := context.WithTimeout(parentCtx, 30*time.Minute)
defer cancel()
q := sqlcdb.New(db.DB)
// The checkout and any shipped artifacts die with this deployment.
var artifactIDs []string
@@ -425,11 +392,10 @@ func runDeploymentAndSyncWithImage(
}()
failDeploy := func(msg string) {
failedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
Error: sql.NullString{String: msg, Valid: true},
CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
ID: dbDeployment.ID,
})
_, _ = db.Exec(
`UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
msg, failedAt, dbDeployment.ID,
)
}
sourcePath := strings.TrimSpace(service.BuildPath)
@@ -457,10 +423,11 @@ func runDeploymentAndSyncWithImage(
replicas = 1
}
var publishedPort int32
// Best effort: reuse the host port from the last live deployment so public
// URLs survive redeploys. Column exists post-migration; older DBs get
// 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)
if service.Builder == "static" && publicPort == 0 {
@@ -484,9 +451,7 @@ func runDeploymentAndSyncWithImage(
// remote builds package it into a context artifact the node builds.
if service.GitRepo != "" && imageOverride == "" {
var ownerID string
if o, err := q.GetProjectOwner(ctx, service.ProjectID); err == nil {
ownerID = o.String()
}
_ = db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, service.ProjectID).Scan(&ownerID)
branch := req.Branch
if branch == "" {
branch = service.GitBranch
@@ -570,9 +535,7 @@ func runDeploymentAndSyncWithImage(
}
} else {
var ownerID string
if o, err := q.GetProjectOwner(ctx, service.ProjectID); err == nil {
ownerID = o.String()
}
_ = db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, service.ProjectID).Scan(&ownerID)
auth := registryAuthFor(db, ownerID, image)
deployReq.BuildConfig = &deployment.BuildConfig{
BuildType: "prebuilt",
@@ -642,11 +605,10 @@ func runDeploymentAndSyncWithImage(
if msg := capacityCheck(parentCtx, db, engine.DockerClient(), service, replicas); msg != "" {
failedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
Error: sql.NullString{String: msg, Valid: true},
CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
ID: dbDeployment.ID,
})
_, _ = db.Exec(
`UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
msg, failedAt, dbDeployment.ID,
)
return
}
@@ -654,16 +616,16 @@ func runDeploymentAndSyncWithImage(
if err != nil {
failedAt := time.Now()
failure := "Failed to start deployment engine: " + err.Error()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
Error: sql.NullString{String: failure, Valid: true},
CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
ID: dbDeployment.ID,
})
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "failed", Valid: true},
UpdatedAt: sql.NullTime{Time: failedAt, Valid: true},
ID: service.ID,
})
_, _ = db.Exec(
`UPDATE deployments
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
WHERE id = $3`,
failure, failedAt, dbDeployment.ID,
)
_, _ = db.Exec(
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
failedAt, service.ID,
)
return
}
@@ -678,17 +640,16 @@ func runDeploymentAndSyncWithImage(
if errors.Is(ctx.Err(), context.Canceled) {
finalStatus, finalErr = "cancelled", "Deployment cancelled"
}
_ = q.CompleteDeployment(ctx, sqlcdb.CompleteDeploymentParams{
Status: sql.NullString{String: finalStatus, Valid: true},
Error: sql.NullString{String: finalErr, Valid: true},
CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
ID: dbDeployment.ID,
})
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: finalStatus, Valid: true},
UpdatedAt: sql.NullTime{Time: failedAt, Valid: true},
ID: service.ID,
})
_, _ = db.Exec(
`UPDATE deployments
SET status = $1, error = $2, completed_at = $3, updated_at = $3
WHERE id = $4`,
finalStatus, finalErr, failedAt, dbDeployment.ID,
)
_, _ = db.Exec(
`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`,
finalStatus, failedAt, service.ID,
)
return
case <-syncTicker.C:
current, getErr := engine.GetDeployment(engineDeployment.ID)
@@ -699,36 +660,49 @@ func runDeploymentAndSyncWithImage(
dbStatus := mapEngineStatusToDBStatus(current.Status)
imageName, imageTag := splitImageReference(current.ImageName, dbDeployment.ImageTag)
now := time.Now()
_ = q.SyncDeploymentProgress(ctx, sqlcdb.SyncDeploymentProgressParams{
Status: sql.NullString{String: dbStatus, Valid: true},
ImageName: sql.NullString{String: imageName, Valid: true},
ImageTag: sql.NullString{String: imageTag, Valid: true},
BuildLog: sql.NullString{String: current.BuildLog, Valid: true},
RuntimeLog: sql.NullString{String: current.DeployLog, Valid: true},
Error: sql.NullString{String: current.Error, Valid: current.Error != ""},
StartedAt: ntPtr(current.StartedAt),
CompletedAt: ntPtr(current.CompletedAt),
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: dbDeployment.ID,
})
var dbError interface{}
if current.Error != "" {
dbError = current.Error
}
_, _ = db.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`,
dbStatus,
imageName,
imageTag,
current.BuildLog,
current.DeployLog,
dbError,
current.StartedAt,
current.CompletedAt,
time.Now(),
dbDeployment.ID,
)
switch dbStatus {
case "deployed":
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "running", Valid: true},
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: service.ID,
})
_, _ = db.Exec(
`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`,
time.Now(), service.ID,
)
insertUserNotification(db, userID, "deployment", "Deployment succeeded",
fmt.Sprintf("Service %s is now running.", service.Name), "service", service.ID.String())
return
case "failed":
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "failed", Valid: true},
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: service.ID,
})
_, _ = db.Exec(
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
time.Now(), service.ID,
)
body := fmt.Sprintf("Service %s failed to deploy.", service.Name)
if 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
}
ctx := c.Request.Context()
q := sqlcdb.New(db.(*database.DB).DB)
access, err := q.GetDeploymentAccess(ctx, deploymentID)
var status, owner string
var serviceID uuid.UUID
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found", "code": "NOT_FOUND"})
return
}
serviceID := access.ServiceID
status := access.Status.String
if access.OwnerID.String() != userID.(string) && !contextIsAdmin(c) {
if owner != userID.(string) && !contextIsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied", "code": "FORBIDDEN"})
return
}
@@ -803,17 +782,14 @@ func handleCancelDeployment(c *gin.Context) {
switch getDeployQueue(c).Cancel(serviceID, deploymentID) {
case deployqueue.WasQueued:
now := time.Now()
_ = q.CompleteDeployment(ctx, sqlcdb.CompleteDeploymentParams{
Status: sql.NullString{String: "cancelled", Valid: true},
Error: sql.NullString{String: "Deployment cancelled", Valid: true},
CompletedAt: sql.NullTime{Time: now, Valid: true},
ID: deploymentID,
})
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "cancelled", Valid: true},
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: serviceID,
})
_, _ = db.(*database.DB).Exec(
`UPDATE deployments SET status = 'cancelled', error = 'Deployment cancelled', completed_at = $1, updated_at = $1 WHERE id = $2`,
now, deploymentID,
)
_, _ = db.(*database.DB).Exec(
`UPDATE services SET status = 'cancelled', updated_at = $1 WHERE id = $2`,
now, serviceID,
)
c.JSON(http.StatusOK, gin.H{"status": "cancelled"})
case deployqueue.WasActive:
c.JSON(http.StatusAccepted, gin.H{"status": "cancelling"})
@@ -856,18 +832,27 @@ func handleGetDeployment(c *gin.Context) {
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
@@ -897,23 +882,33 @@ func handleRollbackDeployment(c *gin.Context) {
return
}
ctx := c.Request.Context()
q := sqlcdb.New(db.(*database.DB).DB)
r, err := q.GetDeploymentForRollback(ctx, deploymentID)
var targetDeployment DeploymentModel
var serviceID uuid.UUID
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
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"})
return
}
@@ -936,28 +931,23 @@ func handleRollbackDeployment(c *gin.Context) {
UpdatedAt: now,
}
err = q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{
ID: rollback.ID,
ServiceID: rollback.ServiceID,
Version: fmt.Sprintf("rollback-%d", now.Unix()),
CommitHash: sql.NullString{String: ptrStr(rollback.CommitHash), Valid: rollback.CommitHash != nil},
Status: sql.NullString{String: rollback.Status, Valid: true},
ImageName: sql.NullString{String: rollback.ImageName, Valid: true},
ImageTag: sql.NullString{String: rollback.ImageTag, Valid: true},
CreatedAt: sql.NullTime{Time: rollback.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: rollback.UpdatedAt, Valid: true},
})
_, err = db.(*database.DB).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)`,
rollback.ID, rollback.ServiceID, fmt.Sprintf("rollback-%d", now.Unix()), rollback.CommitHash, rollback.Status,
rollback.ImageName, rollback.ImageTag, rollback.CreatedAt, rollback.UpdatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create rollback deployment"})
return
}
err = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "building", Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: serviceID,
})
_, err = db.(*database.DB).Exec(
`UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
time.Now(), serviceID,
)
// Real rollback: redeploy the target deployment's image.
engineValue, _ := c.Get("deployment_engine")
@@ -973,18 +963,36 @@ func handleRollbackDeployment(c *gin.Context) {
if engine == nil {
reason = "Deployment engine unavailable. Docker may not be configured on this server."
}
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
Error: sql.NullString{String: reason, Valid: true},
CompletedAt: sql.NullTime{Time: completedAt, Valid: true},
ID: rollbackID,
})
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "failed", Valid: true},
UpdatedAt: sql.NullTime{Time: completedAt, Valid: true},
ID: serviceID,
})
_, _ = db.(*database.DB).Exec(
`UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
reason, completedAt, rollbackID,
)
_, _ = db.(*database.DB).Exec(
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
completedAt, serviceID,
)
} 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{
CommitHash: func() string {
+1 -262
View File
@@ -2,11 +2,10 @@ package api
import (
"context"
"database/sql"
"os"
"strings"
"testing"
"time"
"testing"
"containr/internal/database"
"containr/internal/database/sqlcdb"
@@ -339,263 +338,3 @@ func TestPreviewEnvironmentsLiveInvariants(t *testing.T) {
}
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 (
"containr/internal/database"
"containr/internal/database/sqlcdb"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
@@ -94,73 +92,41 @@ type CreateWebhookRequest struct {
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) {
ctx, ok := requireGitRequestContext(c)
if !ok {
return
}
userID, err := uuid.Parse(ctx.userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
rows, err := sqlcdb.New(ctx.db.DB).ListGitProviders(context.Background(), userID)
rows, err := ctx.db.Query(`
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
`, ctx.userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
defer rows.Close()
providers := make([]GitProvider, 0, len(rows))
for _, p := range rows {
providers = append(providers, gitProviderFrom(p))
providers := make([]GitProvider, 0)
for rows.Next() {
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})
@@ -207,22 +173,21 @@ func handleConnectGitHubApp(c *gin.Context) {
UserID: ctx.userID,
}
prow, err := sqlcdb.New(ctx.db.DB).UpsertGitHubAppProvider(context.Background(), sqlcdb.UpsertGitHubAppProviderParams{
ID: uuid.MustParse(provider.ID),
Name: provider.Name,
DisplayName: provider.DisplayName,
ApiUrl: provider.APIUrl,
WebhookUrl: provider.WebhookUrl,
AccessToken: provider.AccessToken,
UserID: uuid.MustParse(provider.UserID),
})
if err != nil {
if err := ctx.db.QueryRow(`
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
`, 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"})
return
}
provider.ID = prow.ID.String()
provider.CreatedAt = prow.CreatedAt.Time
provider.UpdatedAt = prow.UpdatedAt.Time
provider.AccessToken = ""
c.JSON(http.StatusOK, gin.H{
@@ -294,16 +259,10 @@ func handleCreateGitProvider(c *gin.Context) {
return
}
q := sqlcdb.New(ctx.db.DB)
if err := q.InsertGitProvider(context.Background(), sqlcdb.InsertGitProviderParams{
ID: uuid.MustParse(provider.ID),
Name: provider.Name,
DisplayName: provider.DisplayName,
ApiUrl: provider.APIUrl,
WebhookUrl: provider.WebhookUrl,
AccessToken: provider.AccessToken,
UserID: uuid.MustParse(provider.UserID),
}); err != nil {
if _, err := ctx.db.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())
`, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl, provider.WebhookUrl, provider.AccessToken, provider.UserID); err != nil {
if isUniqueConstraintError(err) {
c.JSON(http.StatusConflict, gin.H{"error": "Provider already connected for this account"})
return
@@ -312,12 +271,13 @@ func handleCreateGitProvider(c *gin.Context) {
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"})
return
} else {
provider.CreatedAt = ts.CreatedAt.Time
provider.UpdatedAt = ts.UpdatedAt.Time
}
provider.AccessToken = ""
@@ -336,11 +296,12 @@ func handleGetGitRepositories(c *gin.Context) {
return
}
q := sqlcdb.New(ctx.db.DB)
provRow, err := q.GetGitProviderForRepos(context.Background(), sqlcdb.GetGitProviderForReposParams{
ID: uuid.MustParse(providerID),
UserID: uuid.MustParse(ctx.userID),
})
var providerName, providerDisplayName, providerAccessToken, providerAPIURL string
err := ctx.db.QueryRow(`
SELECT name, display_name, access_token, api_url
FROM git_providers
WHERE id = $1 AND user_id = $2
`, providerID, ctx.userID).Scan(&providerName, &providerDisplayName, &providerAccessToken, &providerAPIURL)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
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"})
return
}
providerName := provRow.Name
providerDisplayName := provRow.DisplayName
providerAccessToken := provRow.AccessToken
providerAPIURL := provRow.ApiUrl
if c.DefaultQuery("sync", "1") != "0" {
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
search := strings.TrimSpace(c.Query("search"))
searchNullable := sql.NullString{String: search, Valid: search != ""}
repoRows, err := q.ListGitRepositories(context.Background(), sqlcdb.ListGitRepositoriesParams{
ProviderID: uuid.MustParse(providerID),
UserID: uuid.MustParse(ctx.userID),
Column3: search,
Limit: int32(limit),
Offset: int32(offset),
})
rows, err := ctx.db.Query(`
SELECT id, provider_id, name, full_name, COALESCE(description, ''), clone_url, COALESCE(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
`, providerID, ctx.userID, searchNullable, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
defer rows.Close()
repositories := make([]GitRepository, 0, len(repoRows))
for _, r := range repoRows {
repositories = append(repositories, gitRepoFrom(r))
repositories := make([]GitRepository, 0)
for rows.Next() {
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{
ProviderID: uuid.MustParse(providerID),
UserID: uuid.MustParse(ctx.userID),
Column3: search,
})
if err != nil {
var total int
if err := ctx.db.QueryRow(`
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 || '%'))
`, providerID, ctx.userID, searchNullable).Scan(&total); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
@@ -428,11 +408,12 @@ func handleConnectGitRepository(c *gin.Context) {
return
}
q := sqlcdb.New(ctx.db.DB)
providerName, err := q.GetGitProviderName(context.Background(), sqlcdb.GetGitProviderNameParams{
ID: uuid.MustParse(providerID),
UserID: uuid.MustParse(ctx.userID),
})
var providerName string
err := ctx.db.QueryRow(`
SELECT name
FROM git_providers
WHERE id = $1 AND user_id = $2
`, providerID, ctx.userID).Scan(&providerName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
@@ -448,16 +429,31 @@ func handleConnectGitRepository(c *gin.Context) {
return
}
existingRow, err := q.GetGitRepositoryByFullName(context.Background(), sqlcdb.GetGitRepositoryByFullNameParams{
ProviderID: uuid.MustParse(providerID),
FullName: fullName,
UserID: uuid.MustParse(ctx.userID),
})
var existing GitRepository
err = ctx.db.QueryRow(`
SELECT id, provider_id, name, full_name, COALESCE(description, ''), clone_url, COALESCE(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
`, 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 {
case err == nil:
c.JSON(http.StatusOK, gin.H{
"message": "Repository already connected",
"repository": gitRepoFromFullNameRow(existingRow),
"repository": existing,
})
return
case !errors.Is(err, sql.ErrNoRows):
@@ -477,18 +473,13 @@ func handleConnectGitRepository(c *gin.Context) {
UserID: ctx.userID,
}
ins, err := q.InsertGitRepository(context.Background(), sqlcdb.InsertGitRepositoryParams{
ID: uuid.MustParse(repo.ID),
ProviderID: uuid.MustParse(repo.ProviderID),
Name: repo.Name,
FullName: repo.FullName,
Description: sql.NullString{String: repo.Description, Valid: true},
CloneUrl: repo.CloneURL,
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 err := ctx.db.QueryRow(`
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
`, 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 {
if isUniqueConstraintError(err) {
c.JSON(http.StatusConflict, gin.H{"error": "Repository is already connected"})
return
@@ -496,8 +487,6 @@ func handleConnectGitRepository(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"})
return
}
repo.CreatedAt = ins.CreatedAt.Time
repo.UpdatedAt = ins.UpdatedAt.Time
c.JSON(http.StatusCreated, gin.H{
"message": "Repository connected successfully",
@@ -527,10 +516,12 @@ func handleCreateWebhook(c *gin.Context) {
return
}
providerUUID, err := sqlcdb.New(ctx.db.DB).GetGitRepositoryProviderID(context.Background(), sqlcdb.GetGitRepositoryProviderIDParams{
ID: uuid.MustParse(repoID),
UserID: uuid.MustParse(ctx.userID),
})
var providerID string
err := ctx.db.QueryRow(`
SELECT provider_id
FROM git_repositories
WHERE id = $1 AND user_id = $2
`, repoID, ctx.userID).Scan(&providerID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
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"})
return
}
providerID := providerUUID.String()
eventsJSON, err := json.Marshal(req.Events)
if err != nil {
@@ -562,22 +552,23 @@ func handleCreateWebhook(c *gin.Context) {
Active: true,
}
wrow, err := sqlcdb.New(ctx.db.DB).UpsertGitWebhook(context.Background(), sqlcdb.UpsertGitWebhookParams{
ID: uuid.MustParse(webhook.ID),
RepoID: uuid.MustParse(webhook.RepoID),
ProviderID: uuid.MustParse(webhook.ProviderID),
Events: webhook.Events,
WebhookSecret: webhook.Secret,
BranchFilter: sql.NullString{String: strings.TrimSpace(req.Branch), Valid: true},
})
if err != nil {
if err := ctx.db.QueryRow(`
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
`, 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"})
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{
"message": "Webhook configured successfully",
@@ -607,39 +598,69 @@ func handleGetConnectedRepositories(c *gin.Context) {
}
offset := (page - 1) * limit
q := sqlcdb.New(ctx.db.DB)
repoRows, err := q.ListConnectedRepositories(context.Background(), sqlcdb.ListConnectedRepositoriesParams{
UserID: uuid.MustParse(ctx.userID),
Limit: int32(limit),
Offset: int32(offset),
})
rows, err := ctx.db.Query(`
SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.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
`, ctx.userID, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
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{}{
"id": r.ID.String(),
"provider_id": r.ProviderID.String(),
"name": r.Name,
"full_name": r.FullName,
"description": r.Description,
"clone_url": r.CloneUrl,
"default_branch": r.DefaultBranch.String,
"is_private": r.IsPrivate.Bool,
"created_at": r.CreatedAt.Time,
"updated_at": r.UpdatedAt.Time,
"id": repo.ID,
"provider_id": repo.ProviderID,
"name": repo.Name,
"full_name": repo.FullName,
"description": repo.Description,
"clone_url": repo.CloneURL,
"default_branch": repo.DefaultBranch,
"is_private": repo.IsPrivate,
"created_at": repo.CreatedAt,
"updated_at": repo.UpdatedAt,
"provider": map[string]string{
"name": r.ProviderName,
"display_name": r.DisplayName,
"name": providerName,
"display_name": providerDisplayName,
},
})
}
total, err := q.CountUserRepositories(context.Background(), uuid.MustParse(ctx.userID))
if err != nil {
var total int
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"})
return
}
@@ -684,17 +705,20 @@ func syncProviderRepositories(db *database.DB, providerID, userID, providerName,
defaultBranch = "main"
}
err := sqlcdb.New(db.DB).UpsertGitRepository(context.Background(), sqlcdb.UpsertGitRepositoryParams{
ID: uuid.New(),
ProviderID: uuid.MustParse(providerID),
Name: repository.Name,
FullName: repository.FullName,
Description: sql.NullString{String: repository.Description, Valid: true},
CloneUrl: cloneURL,
DefaultBranch: sql.NullString{String: defaultBranch, Valid: true},
IsPrivate: sql.NullBool{Bool: repository.IsPrivate, Valid: true},
UserID: uuid.MustParse(userID),
})
_, err := db.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()
`, uuid.NewString(), providerID, repository.Name, repository.FullName, repository.Description, cloneURL, defaultBranch, repository.IsPrivate, userID)
if err != nil {
return err
}
@@ -1299,15 +1323,16 @@ func handleDeleteGitProvider(c *gin.Context) {
return
}
affected, err := sqlcdb.New(ctx.db.DB).DeleteGitProvider(context.Background(), sqlcdb.DeleteGitProviderParams{
ID: uuid.MustParse(providerID),
UserID: uuid.MustParse(ctx.userID),
})
result, err := ctx.db.Exec(
"DELETE FROM git_providers WHERE id = $1 AND user_id = $2",
providerID, ctx.userID,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"})
return
}
affected, _ := result.RowsAffected()
if affected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
return
@@ -1337,10 +1362,12 @@ func handleGetGitRepositoryBranches(c *gin.Context) {
return
}
pf, err := sqlcdb.New(ctx.db.DB).GetGitProviderForFetch(context.Background(), sqlcdb.GetGitProviderForFetchParams{
ID: uuid.MustParse(providerID),
UserID: uuid.MustParse(ctx.userID),
})
var providerName, providerAPIURL, providerAccessToken string
err := ctx.db.QueryRow(`
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 errors.Is(err, sql.ErrNoRows) {
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"})
return
}
providerName := pf.Name
providerAPIURL := pf.ApiUrl
providerAccessToken := pf.AccessToken
branches, err := fetchRepositoryBranches(providerName, providerAPIURL, providerAccessToken, owner, repo)
if err != nil {
+64 -69
View File
@@ -5,7 +5,6 @@ import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
@@ -15,7 +14,6 @@ import (
"time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment"
"containr/internal/deployqueue"
@@ -40,15 +38,22 @@ func handleGitWebhookPush(c *gin.Context) {
return
}
q := sqlcdb.New(db.DB)
hook, err := q.GetGitWebhookForPush(context.Background(), uuid.MustParse(webhookID))
var (
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"})
return
}
secret := hook.WebhookSecret
branchFilter := hook.BranchFilter
if !hook.Active.Bool {
if !active {
c.JSON(http.StatusForbidden, gin.H{"error": "Webhook is disabled"})
return
}
@@ -59,7 +64,8 @@ func handleGitWebhookPush(c *gin.Context) {
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) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"})
@@ -112,12 +118,14 @@ func handleGitWebhookPush(c *gin.Context) {
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
return
}
cloneURL, fullName, repoUserID := repo.CloneUrl, repo.FullName, repo.UserID.String()
engineValue, _ := c.Get("deployment_engine")
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
// GitHub App webhook endpoint.
func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.DeploymentEngine, cloneURL, fullName, branch, commit, repoUserID string) (int, error) {
q := sqlcdb.New(db.DB)
ctx := context.Background()
// Sync-style events may carry no branch — empty branch matches all.
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},
})
// Sync-style events may carry no branch — match services on repo only.
branchClause := "s.git_branch = $1"
if branch == "" {
branchClause = "TRUE"
}
rows, err := db.Query(fmt.Sprintf(`
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 {
return 0, err
}
defer rows.Close()
var services []Service
for _, row := range rows {
services = append(services, Service{
ID: row.ID,
ProjectID: row.ProjectID,
Name: row.Name,
Type: row.Type,
Status: row.Status,
Image: row.Image,
Command: row.Command,
Environment: row.Environment,
GitRepo: row.GitRepo,
GitBranch: row.GitBranch,
BuildPath: row.BuildPath,
CPU: row.Cpu,
Memory: row.Memory,
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
})
for rows.Next() {
var s Service
if err := rows.Scan(
&s.ID, &s.ProjectID, &s.Name, &s.Type, &s.Status, &s.Image, &s.Command,
&s.Environment, &s.GitRepo, &s.GitBranch, &s.BuildPath, &s.CPU, &s.Memory,
&s.CreatedAt, &s.UpdatedAt,
); err != nil {
return 0, err
}
services = append(services, s)
}
enqueued := 0
@@ -187,34 +192,27 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.
CreatedAt: now,
UpdatedAt: now,
}
if err := q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{
ID: d.ID,
ServiceID: d.ServiceID,
Version: fmt.Sprintf("v%d", now.Unix()),
CommitHash: sql.NullString{String: ptrStr(d.CommitHash), Valid: d.CommitHash != nil},
Status: sql.NullString{String: d.Status, Valid: true},
ImageName: sql.NullString{String: d.ImageName, Valid: true},
ImageTag: sql.NullString{String: d.ImageTag, Valid: true},
CreatedAt: sql.NullTime{Time: d.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: d.UpdatedAt, Valid: true},
}); err != nil {
if _, err := db.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)`,
d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt,
); err != nil {
continue
}
if engine == nil {
failure := "Deployment engine unavailable. Docker may not be configured on this server."
failedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{
Error: sql.NullString{String: failure, Valid: true},
CompletedAt: sql.NullTime{Time: failedAt, Valid: true},
ID: d.ID,
})
failure := "Deployment engine unavailable. Docker may not be configured on this server."
_, _ = db.Exec(
`UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
failure, failedAt, d.ID,
)
continue
}
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "building", Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: service.ID,
})
_, _ = db.Exec(
`UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
time.Now(), service.ID,
)
// Webhook pushes default to a clean build — stale layers are the
// classic silent-rollback failure (dflow convention).
branchForBuild := branch
@@ -228,17 +226,14 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.
runDeploymentAndSync(jctx, db, engine, &d, service, webhookReq, repoUserID)
},
}); pos > 0 {
now := sql.NullTime{Time: time.Now(), Valid: true}
_ = q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{
Status: sql.NullString{String: "queued", Valid: true},
UpdatedAt: now,
ID: d.ID,
})
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: "queued", Valid: true},
UpdatedAt: now,
ID: service.ID,
})
_, _ = db.Exec(
`UPDATE deployments SET status = 'queued', updated_at = $1 WHERE id = $2`,
time.Now(), d.ID,
)
_, _ = db.Exec(
`UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`,
time.Now(), service.ID,
)
}
enqueued++
}
+1 -203
View File
@@ -1,16 +1,6 @@
package api
import (
"context"
"database/sql"
"strings"
"testing"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
)
import "testing"
func TestNormalizeRepositoryFullName(t *testing.T) {
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)
}
}
@@ -775,7 +775,11 @@ func resolvePreviewHost(db *database.DB, cloneID uuid.UUID, envName string) stri
// loadServiceForDeploy hydrates the Service fields runDeploymentAndSync
// needs — mirrors the loader in handleDeployService.
func serviceForDeployFromRow(r sqlcdb.GetServiceForDeployRow) Service {
func loadServiceForDeploy(db *database.DB, serviceID uuid.UUID) (Service, error) {
r, err := sqlcdb.New(db.DB).GetServiceForDeploy(context.Background(), serviceID)
if err != nil {
return Service{}, err
}
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,
@@ -787,15 +791,7 @@ func serviceForDeployFromRow(r sqlcdb.GetServiceForDeployRow) Service {
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) {
r, err := sqlcdb.New(db.DB).GetServiceForDeploy(context.Background(), serviceID)
if err != nil {
return Service{}, err
}
return serviceForDeployFromRow(r), nil
}, nil
}
// StartPreviewSweeper expires preview environments past their TTL — tears
+197 -103
View File
@@ -2,7 +2,6 @@ package api
import (
"context"
"database/sql"
"fmt"
"net/http"
"regexp"
@@ -11,7 +10,6 @@ import (
"time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment"
"containr/internal/deployqueue"
"containr/internal/secrets"
@@ -24,17 +22,22 @@ import (
// service — covers pinned placements, spread replicas, and leftovers from a
// since-cleared pin.
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 {
return nil
}
out := make([]string, 0, len(ids))
for _, id := range ids {
if id != "" {
out = append(out, id)
defer rows.Close()
var ids []string
for rows.Next() {
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
@@ -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
// public URL survives restarts. Column exists post-migration; older DBs
// silently get ephemeral ports.
if p, err := sqlcdb.New(db.DB).GetServicePublishedPort(context.Background(), service.ID); err == nil {
spec.PublishedPort = p
}
_ = db.QueryRow(`SELECT published_port FROM services WHERE id = $1`, service.ID).Scan(&spec.PublishedPort)
if cmd := strings.TrimSpace(service.Command); 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
// the referenced service, or the builtins HOST (service name alias) and PORT.
func resolveServiceEnv(db *database.DB, service Service) (map[string]string, error) {
q := sqlcdb.New(db.DB)
vars, err := q.ListServiceVariableValues(context.Background(), service.ID)
rows, err := db.Query(
`SELECT key, value FROM environment_variables WHERE service_id = $1`,
service.ID,
)
if err != nil {
return nil, err
}
defer rows.Close()
env := map[string]string{}
for _, v := range vars {
env[v.Key] = secrets.Decrypt(v.Value)
for rows.Next() {
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 {
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 {
byName[strings.ToLower(s.Name)] = s.ID
}
@@ -169,15 +189,17 @@ func resolveServiceEnv(db *database.DB, service Service) (map[string]string, err
case "HOST":
return serviceName, true
case "PORT":
if port, err := q.GetServicePort(context.Background(), id); err == nil && port > 0 {
return strconv.Itoa(int(port)), true
var port int
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
}
value, err := q.GetServiceVariableValue(context.Background(), sqlcdb.GetServiceVariableValueParams{
ServiceID: id,
Key: key,
})
var value string
err := db.QueryRow(
`SELECT value FROM environment_variables WHERE service_id = $1 AND key = $2`,
id, key,
).Scan(&value)
if err != nil {
return "", false
}
@@ -250,15 +272,6 @@ func parseMemoryLimit(mem string) int64 {
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) {
dbValue, exists := c.Get("db")
if !exists {
@@ -273,22 +286,6 @@ func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.D
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) {
serviceID, err := uuid.Parse(c.Param("id"))
if err != nil {
@@ -301,16 +298,43 @@ func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) {
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
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"})
return Service{}, false
}
return serviceFromRuntimeRow(row), true
return service, true
}
// 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
}
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false
}
service := serviceFromRuntimeRow(row)
if _, allowed := projectReadAccess(c, db, service.ProjectID); !allowed {
if _, allowed := projectReadAccess(c, db, projectID); !allowed {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false
}
@@ -379,7 +429,7 @@ func reconcileNow(c *gin.Context, engine *deployment.DeploymentEngine, db *datab
status = state.Status
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})
return true
}
@@ -398,7 +448,7 @@ func handleServiceStart(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
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"})
}
@@ -416,7 +466,7 @@ func handleServiceStop(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
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"})
}
@@ -435,7 +485,7 @@ func handleServiceRestart(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
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"})
}
@@ -455,8 +505,12 @@ func handleServiceRedeploy(c *gin.Context) {
image := service.Image
if image == "" {
// Git-sourced service: reuse the last successful deployment image.
var err error
image, err = sqlcdb.New(db.DB).GetLastDeployedImage(context.Background(), service.ID)
err := db.QueryRow(
`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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No deployable image yet — run a deployment first"})
return
@@ -502,7 +556,7 @@ func reconcileServiceJob(ctx context.Context, db *database.DB, engine *deploymen
if 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
}
@@ -521,9 +575,12 @@ func enqueueServiceRedeploy(c *gin.Context, db *database.DB, service Service) bo
}
image := service.Image
if image == "" {
var err error
image, err = sqlcdb.New(db.DB).GetLastDeployedImage(context.Background(), service.ID)
if err != nil {
if err := db.QueryRow(
`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); err != nil {
return false
}
}
@@ -571,10 +628,10 @@ func persistPublishedPort(db *database.DB, serviceID uuid.UUID, ports []uint16)
if len(ports) == 0 {
return
}
_ = sqlcdb.New(db.DB).SetServicePublishedPort(context.Background(), sqlcdb.SetServicePublishedPortParams{
PublishedPort: int32(ports[0]),
ID: serviceID,
})
_, _ = db.Exec(
`UPDATE services SET published_port = $1 WHERE id = $2 AND published_port <> $1`,
int(ports[0]), serviceID,
)
}
// 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 {
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)
if service.Domain != "" {
@@ -619,22 +676,44 @@ func runtimeScaleService(c *gin.Context, serviceID string, replicas int) error {
return fmt.Errorf("runtime unavailable")
}
q := sqlcdb.New(db.DB)
sid, err := uuid.Parse(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 {
return err
}
row, err := q.GetServiceRuntimeWithOwner(context.Background(), sid)
if err != nil {
return err
}
service := serviceFromRuntimeRow(row)
if service.Image == "" {
image, err := q.GetLastDeployedImage(context.Background(), service.ID)
if err != nil {
if err := db.QueryRow(
`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")
}
service.Image = image
}
spec, err := serviceRuntimeSpec(db, service)
@@ -652,11 +731,7 @@ func runtimeScaleService(c *gin.Context, serviceID string, replicas int) error {
if state != nil {
persistPublishedPort(db, service.ID, state.Ports)
}
_ = q.SetServiceReplicas(context.Background(), sqlcdb.SetServiceReplicasParams{
Replicas: int32(replicas),
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: service.ID,
})
_, _ = db.Exec(`UPDATE services SET replicas = $1, updated_at = $2 WHERE id = $3`, replicas, time.Now(), service.ID)
return nil
}
@@ -675,35 +750,48 @@ func handleServiceEnvCheck(c *gin.Context) {
return
}
q := sqlcdb.New(db.DB)
vars, err := q.ListServiceVariablesWithSecret(context.Background(), service.ID)
rows, err := db.Query(
`SELECT key, value, COALESCE(is_secret, false) FROM environment_variables WHERE service_id = $1`,
service.ID,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var unresolved, empty, unreadable []string
var raw []struct{ key, val string }
for _, v := range vars {
decrypted := secrets.Decrypt(v.Value)
if v.IsSecret && secrets.IsEncrypted(v.Value) && decrypted == v.Value {
for rows.Next() {
var k, v string
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.
unreadable = append(unreadable, v.Key)
unreadable = append(unreadable, k)
continue
}
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.
siblingIDs := map[string]uuid.UUID{}
if refs, err := q.ListProjectServiceRefs(context.Background(), service.ProjectID); err == nil {
for _, s := range refs {
siblingIDs[strings.ToLower(s.Name)] = s.ID
siblingIDs := map[string]string{}
svcRows, err := db.Query(`SELECT id, name FROM services WHERE project_id = $1`, service.ProjectID)
if err == nil {
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 {
id, ok := siblingIDs[strings.ToLower(serviceName)]
@@ -714,14 +802,20 @@ func handleServiceEnvCheck(c *gin.Context) {
return true
}
if strings.EqualFold(key, "PORT") {
port, err := q.GetServicePort(context.Background(), id)
return err == nil && port > 0
var port int
if err := db.QueryRow(`SELECT COALESCE(port, 0) FROM services WHERE id = $1`, id).Scan(&port); err == nil {
return port > 0
}
_, err := q.GetServiceVariableValue(context.Background(), sqlcdb.GetServiceVariableValueParams{
ServiceID: id,
Key: key,
})
return err == nil
return false
}
var n int
if err := db.QueryRow(
`SELECT COUNT(*) FROM environment_variables WHERE service_id = $1 AND key = $2`,
id, key,
).Scan(&n); err == nil {
return n > 0
}
return false
}
for _, kv := range raw {
+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) {
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 {
return
}
var candidates []sleepCandidate
for _, r := range sweepRows {
candidates = append(candidates, sleepCandidate{
ID: r.ID, ProjectID: r.ProjectID, Name: r.Name,
Status: r.Status, IdleMinutes: int(r.SleepIdleMinutes),
})
for rows.Next() {
var s sleepCandidate
if err := rows.Scan(&s.ID, &s.ProjectID, &s.Name, &s.Status, &s.IdleMinutes); err == nil {
candidates = append(candidates, s)
}
}
rows.Close()
q := sqlcdb.New(db.DB)
for _, svc := range candidates {
@@ -184,10 +187,11 @@ func putToSleepRemote(ctx context.Context, db *database.DB, q *sqlcdb.Queries, s
tracker.mu.Lock()
delete(tracker.activity, svc.ID)
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)
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)
if len(domains) > 0 {
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()
delete(tracker.activity, svc.ID)
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)
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)
if len(domains) == 0 {
return
@@ -439,9 +444,7 @@ func pruneWakeContainers(ctx context.Context, db *database.DB, dc *docker.Client
}
}
var status string
if sid, err := uuid.Parse(svcID); err == nil {
status, _ = sqlcdb.New(db.DB).GetServiceStatusText(ctx, sid)
}
_ = db.QueryRow(`SELECT COALESCE(status,'') FROM services WHERE id = $1`, svcID).Scan(&status)
if live > 0 || status == "stopped" || status == "" {
_ = dc.RemoveContainer(ctx, c.ID, true)
}
@@ -488,11 +491,34 @@ func triggerServiceWake(c *gin.Context) (int, gin.H) {
if err != nil {
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 {
return http.StatusNotFound, gin.H{"error": "service not found"}
}
service := serviceFromRuntimeRow(row)
if service.Status == "running" || service.Status == "deployed" {
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)
if err != nil {
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
}
state, err := engine.ReconcileService(ctx, spec)
if err != nil {
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
}
status := "running"
@@ -516,7 +542,7 @@ func triggerServiceWake(c *gin.Context) (int, gin.H) {
status = state.Status
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"}
}
@@ -537,6 +563,8 @@ func handleServiceSleep(c *gin.Context) {
if !ok {
return
}
var port int
_ = db.QueryRow(`SELECT COALESCE(port,0) FROM services WHERE id = $1`, service.ID).Scan(&port)
svc := sleepCandidate{
ID: service.ID,
ProjectID: service.ProjectID,
@@ -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,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
}
@@ -98,34 +98,6 @@ type ContainerInstance struct {
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 {
ID string `json:"id"`
DatabaseID string `json:"database_id"`
@@ -200,46 +172,6 @@ type EnvironmentVariable struct {
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"`
@@ -441,8 +373,6 @@ type Service struct {
StaticDir string `json:"static_dir"`
NodeID sql.NullString `json:"node_id"`
Spread bool `json:"spread"`
SleepEnabled bool `json:"sleep_enabled"`
SleepIdleMinutes int32 `json:"sleep_idle_minutes"`
PlacementTags json.RawMessage `json:"placement_tags"`
TraefikLabels json.RawMessage `json:"traefik_labels"`
}
@@ -22,20 +22,15 @@ type Querier interface {
CloneServiceRow(ctx context.Context, arg CloneServiceRowParams) error
CloneServiceVariables(ctx context.Context, arg CloneServiceVariablesParams) error
CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error)
CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error
CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error)
CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error)
CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error)
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)
CountGitRepositories(ctx context.Context, arg CountGitRepositoriesParams) (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)
CountUnreadNotificationsByUser(ctx context.Context, userID uuid.UUID) (int64, error)
CountUserRepositories(ctx context.Context, userID uuid.UUID) (int64, error)
CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error)
CreateAgentAuthToken(ctx context.Context, arg CreateAgentAuthTokenParams) (AgentAuthToken, error)
CreateBackupTarget(ctx context.Context, arg CreateBackupTargetParams) error
@@ -61,10 +56,8 @@ type Querier interface {
DeleteAllServiceDomains(ctx context.Context, serviceID uuid.UUID) error
DeleteBackupTargetByIDAndUser(ctx context.Context, arg DeleteBackupTargetByIDAndUserParams) error
DeleteBanner(ctx context.Context, id uuid.UUID) error
DeleteCronJob(ctx context.Context, id uuid.UUID) error
DeleteDatabaseServiceByID(ctx context.Context, id string) error
DeleteDatabaseServiceByIDAndUser(ctx context.Context, arg DeleteDatabaseServiceByIDAndUserParams) error
DeleteGitProvider(ctx context.Context, arg DeleteGitProviderParams) (int64, error)
DeleteNotificationChannel(ctx context.Context, arg DeleteNotificationChannelParams) (int64, error)
DeleteOutboundWebhookByIDAndUser(ctx context.Context, arg DeleteOutboundWebhookByIDAndUserParams) error
DeletePreviewEnvironment(ctx context.Context, id uuid.UUID) error
@@ -75,8 +68,6 @@ type Querier interface {
DeleteServiceDomain(ctx context.Context, arg DeleteServiceDomainParams) (int64, error)
DeleteUserInvite(ctx context.Context, id uuid.UUID) 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)
GetAgent(ctx context.Context, id string) (NodeAgent, error)
GetAgentByHostAndIP(ctx context.Context, arg GetAgentByHostAndIPParams) (NodeAgent, error)
@@ -86,29 +77,11 @@ type Querier interface {
GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error)
GetContainer(ctx context.Context, id string) (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)
GetDatabaseServiceByID(ctx context.Context, id string) (DatabaseService, error)
GetDatabaseServiceByIDAndUser(ctx context.Context, arg GetDatabaseServiceByIDAndUserParams) (DatabaseService, 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)
GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error)
GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error)
GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error)
GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error)
@@ -126,26 +99,14 @@ type Querier interface {
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)
GetServiceTypeBrief(ctx context.Context, arg GetServiceTypeBriefParams) (GetServiceTypeBriefRow, error)
GetServiceVariableValue(ctx context.Context, arg GetServiceVariableValueParams) (string, error)
GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error)
GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, 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)
InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error
InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error
@@ -158,21 +119,14 @@ type Querier interface {
ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error)
ListBanners(ctx context.Context) ([]Banner, error)
ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error)
ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, 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)
ListDatabaseServicesByProject(ctx context.Context, projectID uuid.NullUUID) ([]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)
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)
ListNotificationsByUser(ctx context.Context, arg ListNotificationsByUserParams) ([]Notification, error)
@@ -181,12 +135,10 @@ type Querier interface {
ListPendingProjects(ctx context.Context) ([]Project, error)
ListPreviewEnvironmentsForProject(ctx context.Context, projectID uuid.UUID) ([]ListPreviewEnvironmentsForProjectRow, 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
// 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.
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
// placement tags — auto-domain candidates for spread services.
ListSchedulableAgentDomains(ctx context.Context, dollar_1 json.RawMessage) ([]string, error)
@@ -200,10 +152,6 @@ type Querier interface {
ListServiceMetrics(ctx context.Context, arg ListServiceMetricsParams) ([]ListServiceMetricsRow, error)
ListServiceTemplatesByCategoryForUser(ctx context.Context, arg ListServiceTemplatesByCategoryForUserParams) ([]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)
ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error)
@@ -235,19 +183,13 @@ type Querier interface {
SetDatabaseServiceStatusAndConnectionByID(ctx context.Context, arg SetDatabaseServiceStatusAndConnectionByIDParams) error
SetDatabaseServiceStatusByID(ctx context.Context, arg SetDatabaseServiceStatusByIDParams) error
SetDatabaseServiceStatusByIDAndUser(ctx context.Context, arg SetDatabaseServiceStatusByIDAndUserParams) error
SetDeploymentStatus(ctx context.Context, arg SetDeploymentStatusParams) 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
TouchServiceDomainChecked(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)
UpdateAgentHeartbeat(ctx context.Context, arg UpdateAgentHeartbeatParams) error
UpdateBackupTargetByIDAndUser(ctx context.Context, arg UpdateBackupTargetByIDAndUserParams) error
@@ -260,7 +202,6 @@ type Querier interface {
// removed replica. $4 is the cumulative rx+tx byte counter.
UpdateContainerStateByName(ctx context.Context, arg UpdateContainerStateByNameParams) error
UpdateContainerStatus(ctx context.Context, arg UpdateContainerStatusParams) error
UpdateCronJobRun(ctx context.Context, arg UpdateCronJobRunParams) error
UpdateDatabaseServiceNameAndPlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameAndPlanByIDAndUserParams) error
UpdateDatabaseServiceNameByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameByIDAndUserParams) error
UpdateDatabaseServicePlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServicePlanByIDAndUserParams) error
@@ -271,9 +212,6 @@ type Querier interface {
UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error)
UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) 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
UpsertNodeMetrics(ctx context.Context, arg UpsertNodeMetricsParams) error
UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) 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
}
-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;
-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);
-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;
-70
View File
@@ -69,8 +69,6 @@ CREATE TABLE services (
static_dir VARCHAR(255) NOT NULL DEFAULT '',
node_id VARCHAR(255),
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 '[]',
traefik_labels JSONB NOT NULL DEFAULT '{}'
);
@@ -461,71 +459,3 @@ CREATE TABLE instance_metrics (
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()
);