Compare commits

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

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-10 03:52:36 +02:00
6 changed files with 198 additions and 580 deletions

No files matched your search

+1 -1
View File
@@ -537,7 +537,7 @@ and wired refs, one click or one CLI call.
workspace, CLI `environments` + MCP parity. Per-env vars/domains ride the workspace, CLI `environments` + MCP parity. Per-env vars/domains ride the
service's own scope (one env per service); promote ships via preview service's own scope (one env per service); promote ships via preview
promote. promote.
- [ ] **sqlc port** — ~265 raw `database/sql` sites → `sqlc/queries/`. Done in tranches (dynamic queries stay raw). Tranches merged: metrics storage + service domains (#75), service clone/move ops (#76), preview environments (#77), deployments + rollback (#78). Remaining: runtime, compliance, cron, git, security, auth, sleeper, scanner, gateway, and smaller files. - [ ] **sqlc port** — ~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/ - [ ] **Tests** — canvas/wizard frontend coverage, deployment/template/
webhook table tests, Playwright e2e golden path. webhook table tests, Playwright e2e golden path.
- [x] **First-run wizard** — public `GET /setup/status` + authed - [x] **First-run wizard** — public `GET /setup/status` + authed
-130
View File
@@ -444,133 +444,3 @@ func TestDeploymentsLiveInvariants(t *testing.T) {
t.Fatalf("post-fail row: %+v", r) 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)
}
}
+197 -103
View File
@@ -2,7 +2,6 @@ package api
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
@@ -11,7 +10,6 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
"containr/internal/secrets" "containr/internal/secrets"
@@ -24,17 +22,22 @@ import (
// service — covers pinned placements, spread replicas, and leftovers from a // service — covers pinned placements, spread replicas, and leftovers from a
// since-cleared pin. // since-cleared pin.
func remoteAgentsForService(ctx context.Context, db *database.DB, serviceID uuid.UUID) []string { func remoteAgentsForService(ctx context.Context, db *database.DB, serviceID uuid.UUID) []string {
ids, err := sqlcdb.New(db.DB).ListNodeAgentsForService(ctx, serviceID.String()) rows, err := db.Query(
`SELECT DISTINCT node_agent_id FROM container_instances WHERE service_id = $1`,
serviceID,
)
if err != nil { if err != nil {
return nil return nil
} }
out := make([]string, 0, len(ids)) defer rows.Close()
for _, id := range ids { var ids []string
if id != "" { for rows.Next() {
out = append(out, id) var id string
if rows.Scan(&id) == nil && id != "" {
ids = append(ids, id)
} }
} }
return out return ids
} }
// serviceLifecycle routes a start/stop/restart action to every agent holding // serviceLifecycle routes a start/stop/restart action to every agent holding
@@ -96,9 +99,7 @@ func serviceRuntimeSpec(db *database.DB, service Service) (deployment.RuntimeSpe
// Best effort: reuse the host port from the last live deployment so the // Best effort: reuse the host port from the last live deployment so the
// public URL survives restarts. Column exists post-migration; older DBs // public URL survives restarts. Column exists post-migration; older DBs
// silently get ephemeral ports. // silently get ephemeral ports.
if p, err := sqlcdb.New(db.DB).GetServicePublishedPort(context.Background(), service.ID); err == nil { _ = db.QueryRow(`SELECT published_port FROM services WHERE id = $1`, service.ID).Scan(&spec.PublishedPort)
spec.PublishedPort = p
}
if cmd := strings.TrimSpace(service.Command); cmd != "" { if cmd := strings.TrimSpace(service.Command); cmd != "" {
spec.Command = strings.Fields(cmd) spec.Command = strings.Fields(cmd)
} }
@@ -136,23 +137,42 @@ var refVarPattern = regexp.MustCompile(`\$\{\{\s*([A-Za-z0-9_-]+)\.([A-Za-z_][A-
// secrets) and expands ${{service.KEY}} references. KEY may be a variable on // secrets) and expands ${{service.KEY}} references. KEY may be a variable on
// the referenced service, or the builtins HOST (service name alias) and PORT. // the referenced service, or the builtins HOST (service name alias) and PORT.
func resolveServiceEnv(db *database.DB, service Service) (map[string]string, error) { func resolveServiceEnv(db *database.DB, service Service) (map[string]string, error) {
q := sqlcdb.New(db.DB) rows, err := db.Query(
vars, err := q.ListServiceVariableValues(context.Background(), service.ID) `SELECT key, value FROM environment_variables WHERE service_id = $1`,
service.ID,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
env := map[string]string{} env := map[string]string{}
for _, v := range vars { for rows.Next() {
env[v.Key] = secrets.Decrypt(v.Value) var k, v string
if err := rows.Scan(&k, &v); err != nil {
return nil, err
}
env[k] = secrets.Decrypt(v)
} }
refServices, err := q.ListProjectServiceRefs(context.Background(), service.ProjectID) var refServices []struct{ ID, Name string }
svcRows, err := db.Query(
`SELECT id, name FROM services WHERE project_id = $1`,
service.ProjectID,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer svcRows.Close()
for svcRows.Next() {
var s struct{ ID, Name string }
if err := svcRows.Scan(&s.ID, &s.Name); err != nil {
return nil, err
}
refServices = append(refServices, s)
}
byName := map[string]uuid.UUID{} byName := map[string]string{}
for _, s := range refServices { for _, s := range refServices {
byName[strings.ToLower(s.Name)] = s.ID byName[strings.ToLower(s.Name)] = s.ID
} }
@@ -169,15 +189,17 @@ func resolveServiceEnv(db *database.DB, service Service) (map[string]string, err
case "HOST": case "HOST":
return serviceName, true return serviceName, true
case "PORT": case "PORT":
if port, err := q.GetServicePort(context.Background(), id); err == nil && port > 0 { var port int
return strconv.Itoa(int(port)), true if err := db.QueryRow(`SELECT COALESCE(port, 0) FROM services WHERE id = $1`, id).Scan(&port); err == nil && port > 0 {
return strconv.Itoa(port), true
} }
return "", false return "", false
} }
value, err := q.GetServiceVariableValue(context.Background(), sqlcdb.GetServiceVariableValueParams{ var value string
ServiceID: id, err := db.QueryRow(
Key: key, `SELECT value FROM environment_variables WHERE service_id = $1 AND key = $2`,
}) id, key,
).Scan(&value)
if err != nil { if err != nil {
return "", false return "", false
} }
@@ -250,15 +272,6 @@ func parseMemoryLimit(mem string) int64 {
return 0 return 0
} }
// setServiceStatusNow writes the stored status with a fresh updated_at.
func setServiceStatusNow(db *database.DB, id uuid.UUID, status string) {
_ = sqlcdb.New(db.DB).SetServiceStatus(context.Background(), sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: status, Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: id,
})
}
func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.DB, bool) { func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.DB, bool) {
dbValue, exists := c.Get("db") dbValue, exists := c.Get("db")
if !exists { if !exists {
@@ -273,22 +286,6 @@ func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.D
return engineValue.(*deployment.DeploymentEngine), dbValue.(*database.DB), true return engineValue.(*deployment.DeploymentEngine), dbValue.(*database.DB), true
} }
// serviceFromRuntimeRow maps the sqlc runtime row (with legacy-column
// fallbacks applied in SQL) onto the API Service model.
func serviceFromRuntimeRow(r sqlcdb.GetServiceRuntimeWithOwnerRow) Service {
return Service{
ID: r.ID, ProjectID: r.ProjectID, Name: r.Name, Type: r.Type,
Status: r.Status, Image: r.Image, Command: r.Command,
Environment: r.Environment, GitRepo: r.GitRepo, GitBranch: r.GitBranch,
BuildPath: r.BuildPath, CPU: r.Cpu, Memory: r.Memory,
Replicas: int(r.Replicas), Port: int(r.Port), Domain: r.Domain,
HealthCheckPath: r.HealthcheckPath, RestartPolicy: r.RestartPolicy,
Builder: r.Builder, CPUReserve: r.CpuReserve,
MemoryReserve: r.MemoryReserve, StaticBuildCmd: r.StaticBuildCmd,
StaticDir: r.StaticDir, CreatedAt: r.CreatedAt.Time, UpdatedAt: r.UpdatedAt.Time,
}
}
func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) { func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) {
serviceID, err := uuid.Parse(c.Param("id")) serviceID, err := uuid.Parse(c.Param("id"))
if err != nil { if err != nil {
@@ -301,16 +298,43 @@ func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) {
return Service{}, false return Service{}, false
} }
row, err := sqlcdb.New(db.DB).GetServiceRuntimeWithOwner(context.Background(), serviceID) var service Service
var owner string
err = db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(s.memory, ''),
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at, p.owner_id
FROM services s JOIN projects p ON s.project_id = p.id
WHERE s.id = $1`,
serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt, &owner,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false return Service{}, false
} }
if row.OwnerID.String() != userID.(string) && !isAdminUser(db, userID.(string)) { if owner != userID.(string) && !isAdminUser(db, userID.(string)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return Service{}, false return Service{}, false
} }
return serviceFromRuntimeRow(row), true return service, true
} }
// loadReadableService loads a service when the caller may read its project: // loadReadableService loads a service when the caller may read its project:
@@ -322,13 +346,39 @@ func loadReadableService(c *gin.Context, db *database.DB) (Service, bool) {
return Service{}, false return Service{}, false
} }
row, err := sqlcdb.New(db.DB).GetServiceRuntimeWithOwner(context.Background(), serviceID) var service Service
var projectID uuid.UUID
err = db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(s.memory, ''),
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at, s.project_id
FROM services s
WHERE s.id = $1`,
serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt, &projectID,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false return Service{}, false
} }
service := serviceFromRuntimeRow(row) if _, allowed := projectReadAccess(c, db, projectID); !allowed {
if _, allowed := projectReadAccess(c, db, service.ProjectID); !allowed {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false return Service{}, false
} }
@@ -379,7 +429,7 @@ func reconcileNow(c *gin.Context, engine *deployment.DeploymentEngine, db *datab
status = state.Status status = state.Status
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
} }
setServiceStatusNow(db, service.ID, status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, status, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"runtime": state}) c.JSON(http.StatusOK, gin.H{"runtime": state})
return true return true
} }
@@ -398,7 +448,7 @@ func handleServiceStart(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "running") _, _ = db.Exec(`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "running"}) c.JSON(http.StatusOK, gin.H{"status": "running"})
} }
@@ -416,7 +466,7 @@ func handleServiceStop(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "stopped") _, _ = db.Exec(`UPDATE services SET status = 'stopped', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "stopped"}) c.JSON(http.StatusOK, gin.H{"status": "stopped"})
} }
@@ -435,7 +485,7 @@ func handleServiceRestart(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "running") _, _ = db.Exec(`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "running"}) c.JSON(http.StatusOK, gin.H{"status": "running"})
} }
@@ -455,8 +505,12 @@ func handleServiceRedeploy(c *gin.Context) {
image := service.Image image := service.Image
if image == "" { if image == "" {
// Git-sourced service: reuse the last successful deployment image. // Git-sourced service: reuse the last successful deployment image.
var err error err := db.QueryRow(
image, err = sqlcdb.New(db.DB).GetLastDeployedImage(context.Background(), service.ID) `SELECT image_name || ':' || image_tag FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`,
service.ID,
).Scan(&image)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No deployable image yet — run a deployment first"}) c.JSON(http.StatusBadRequest, gin.H{"error": "No deployable image yet — run a deployment first"})
return return
@@ -502,7 +556,7 @@ func reconcileServiceJob(ctx context.Context, db *database.DB, engine *deploymen
if state.Status != "" { if state.Status != "" {
status = state.Status status = state.Status
} }
setServiceStatusNow(db, service.ID, status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, status, time.Now(), service.ID)
} }
return state, nil return state, nil
} }
@@ -521,9 +575,12 @@ func enqueueServiceRedeploy(c *gin.Context, db *database.DB, service Service) bo
} }
image := service.Image image := service.Image
if image == "" { if image == "" {
var err error if err := db.QueryRow(
image, err = sqlcdb.New(db.DB).GetLastDeployedImage(context.Background(), service.ID) `SELECT image_name || ':' || image_tag FROM deployments
if err != nil { WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`,
service.ID,
).Scan(&image); err != nil {
return false return false
} }
} }
@@ -571,10 +628,10 @@ func persistPublishedPort(db *database.DB, serviceID uuid.UUID, ports []uint16)
if len(ports) == 0 { if len(ports) == 0 {
return return
} }
_ = sqlcdb.New(db.DB).SetServicePublishedPort(context.Background(), sqlcdb.SetServicePublishedPortParams{ _, _ = db.Exec(
PublishedPort: int32(ports[0]), `UPDATE services SET published_port = $1 WHERE id = $2 AND published_port <> $1`,
ID: serviceID, int(ports[0]), serviceID,
}) )
} }
// liveServiceStatus reconciles the stored status with real container state. // liveServiceStatus reconciles the stored status with real container state.
@@ -597,7 +654,7 @@ func liveServiceStatus(c *gin.Context, db *database.DB, service *Service) {
} }
if state.Status != service.Status { if state.Status != service.Status {
service.Status = state.Status service.Status = state.Status
setServiceStatusNow(db, service.ID, state.Status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, state.Status, time.Now(), service.ID)
} }
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
if service.Domain != "" { if service.Domain != "" {
@@ -619,22 +676,44 @@ func runtimeScaleService(c *gin.Context, serviceID string, replicas int) error {
return fmt.Errorf("runtime unavailable") return fmt.Errorf("runtime unavailable")
} }
q := sqlcdb.New(db.DB) var service Service
sid, err := uuid.Parse(serviceID) var owner string
err := db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(s.memory, ''),
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at, p.owner_id
FROM services s JOIN projects p ON s.project_id = p.id
WHERE s.id = $1`, serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt, &owner,
)
if err != nil { if err != nil {
return err return err
} }
row, err := q.GetServiceRuntimeWithOwner(context.Background(), sid)
if err != nil {
return err
}
service := serviceFromRuntimeRow(row)
if service.Image == "" { if service.Image == "" {
image, err := q.GetLastDeployedImage(context.Background(), service.ID) if err := db.QueryRow(
if err != nil { `SELECT image_name || ':' || image_tag FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`, service.ID,
).Scan(&service.Image); err != nil {
return fmt.Errorf("no deployed image to scale") return fmt.Errorf("no deployed image to scale")
} }
service.Image = image
} }
spec, err := serviceRuntimeSpec(db, service) spec, err := serviceRuntimeSpec(db, service)
@@ -652,11 +731,7 @@ func runtimeScaleService(c *gin.Context, serviceID string, replicas int) error {
if state != nil { if state != nil {
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
} }
_ = q.SetServiceReplicas(context.Background(), sqlcdb.SetServiceReplicasParams{ _, _ = db.Exec(`UPDATE services SET replicas = $1, updated_at = $2 WHERE id = $3`, replicas, time.Now(), service.ID)
Replicas: int32(replicas),
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: service.ID,
})
return nil return nil
} }
@@ -675,35 +750,48 @@ func handleServiceEnvCheck(c *gin.Context) {
return return
} }
q := sqlcdb.New(db.DB) rows, err := db.Query(
vars, err := q.ListServiceVariablesWithSecret(context.Background(), service.ID) `SELECT key, value, COALESCE(is_secret, false) FROM environment_variables WHERE service_id = $1`,
service.ID,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
defer rows.Close()
var unresolved, empty, unreadable []string var unresolved, empty, unreadable []string
var raw []struct{ key, val string } var raw []struct{ key, val string }
for _, v := range vars { for rows.Next() {
decrypted := secrets.Decrypt(v.Value) var k, v string
if v.IsSecret && secrets.IsEncrypted(v.Value) && decrypted == v.Value { var secret bool
if err := rows.Scan(&k, &v, &secret); err != nil {
continue
}
decrypted := secrets.Decrypt(v)
if secret && secrets.IsEncrypted(v) && decrypted == v {
// Ciphertext failed to decrypt — Decrypt returns the input. // Ciphertext failed to decrypt — Decrypt returns the input.
unreadable = append(unreadable, v.Key) unreadable = append(unreadable, k)
continue continue
} }
if strings.TrimSpace(decrypted) == "" { if strings.TrimSpace(decrypted) == "" {
empty = append(empty, v.Key) empty = append(empty, k)
} }
raw = append(raw, struct{ key, val string }{v.Key, decrypted}) raw = append(raw, struct{ key, val string }{k, decrypted})
} }
// Project siblings for ${{name.KEY}} resolution. // Project siblings for ${{name.KEY}} resolution.
siblingIDs := map[string]uuid.UUID{} siblingIDs := map[string]string{}
if refs, err := q.ListProjectServiceRefs(context.Background(), service.ProjectID); err == nil { svcRows, err := db.Query(`SELECT id, name FROM services WHERE project_id = $1`, service.ProjectID)
for _, s := range refs { if err == nil {
siblingIDs[strings.ToLower(s.Name)] = s.ID for svcRows.Next() {
var id, name string
if err := svcRows.Scan(&id, &name); err == nil {
siblingIDs[strings.ToLower(name)] = id
} }
} }
svcRows.Close()
}
resolvable := func(serviceName, key string) bool { resolvable := func(serviceName, key string) bool {
id, ok := siblingIDs[strings.ToLower(serviceName)] id, ok := siblingIDs[strings.ToLower(serviceName)]
@@ -714,14 +802,20 @@ func handleServiceEnvCheck(c *gin.Context) {
return true return true
} }
if strings.EqualFold(key, "PORT") { if strings.EqualFold(key, "PORT") {
port, err := q.GetServicePort(context.Background(), id) var port int
return err == nil && port > 0 if err := db.QueryRow(`SELECT COALESCE(port, 0) FROM services WHERE id = $1`, id).Scan(&port); err == nil {
return port > 0
} }
_, err := q.GetServiceVariableValue(context.Background(), sqlcdb.GetServiceVariableValueParams{ return false
ServiceID: id, }
Key: key, var n int
}) if err := db.QueryRow(
return err == nil `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 { for _, kv := range raw {
@@ -89,7 +89,6 @@ type Querier interface {
GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error) GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error)
GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error) GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error)
GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error) GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error)
GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error)
GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error) GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error)
GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error) GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error)
GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error) GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error)
@@ -111,12 +110,9 @@ type Querier interface {
GetServiceForDeploy(ctx context.Context, id uuid.UUID) (GetServiceForDeployRow, error) GetServiceForDeploy(ctx context.Context, id uuid.UUID) (GetServiceForDeployRow, error)
GetServiceForDeployWithOwner(ctx context.Context, id uuid.UUID) (GetServiceForDeployWithOwnerRow, error) GetServiceForDeployWithOwner(ctx context.Context, id uuid.UUID) (GetServiceForDeployWithOwnerRow, error)
GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error) GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error)
GetServicePort(ctx context.Context, id uuid.UUID) (int32, error)
GetServicePublishedPort(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)
GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error) GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error)
GetServiceTypeBrief(ctx context.Context, arg GetServiceTypeBriefParams) (GetServiceTypeBriefRow, 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) GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error)
GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error) GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error)
InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error
@@ -142,7 +138,6 @@ type Querier interface {
ListEnabledOutboundWebhooks(ctx context.Context) ([]OutboundWebhook, error) ListEnabledOutboundWebhooks(ctx context.Context) ([]OutboundWebhook, error)
ListExpiredPreviewsForUser(ctx context.Context, arg ListExpiredPreviewsForUserParams) ([]ListExpiredPreviewsForUserRow, error) ListExpiredPreviewsForUser(ctx context.Context, arg ListExpiredPreviewsForUserParams) ([]ListExpiredPreviewsForUserRow, error)
ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error) ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error)
ListNodeAgentsForService(ctx context.Context, serviceID string) ([]string, error)
ListNodeMetrics(ctx context.Context, arg ListNodeMetricsParams) ([]ListNodeMetricsRow, error) ListNodeMetrics(ctx context.Context, arg ListNodeMetricsParams) ([]ListNodeMetricsRow, error)
ListNotificationChannelsByUser(ctx context.Context, userID uuid.UUID) ([]NotificationChannel, error) ListNotificationChannelsByUser(ctx context.Context, userID uuid.UUID) ([]NotificationChannel, error)
ListNotificationsByUser(ctx context.Context, arg ListNotificationsByUserParams) ([]Notification, error) ListNotificationsByUser(ctx context.Context, arg ListNotificationsByUserParams) ([]Notification, error)
@@ -151,7 +146,6 @@ type Querier interface {
ListPendingProjects(ctx context.Context) ([]Project, error) ListPendingProjects(ctx context.Context) ([]Project, error)
ListPreviewEnvironmentsForProject(ctx context.Context, projectID uuid.UUID) ([]ListPreviewEnvironmentsForProjectRow, error) ListPreviewEnvironmentsForProject(ctx context.Context, projectID uuid.UUID) ([]ListPreviewEnvironmentsForProjectRow, error)
ListProjectEnvironments(ctx context.Context, projectID uuid.UUID) ([]ListProjectEnvironmentsRow, error) ListProjectEnvironments(ctx context.Context, projectID uuid.UUID) ([]ListProjectEnvironmentsRow, error)
ListProjectServiceRefs(ctx context.Context, projectID uuid.UUID) ([]ListProjectServiceRefsRow, error)
// Visibility model: a project is readable when it is approved for public // Visibility model: a project is readable when it is approved for public
// display, when the caller owns or is a member of it, or when the caller is a // display, when the caller owns or is a member of it, or when the caller is a
// platform admin. Anonymous callers pass user_id = uuid.Nil, is_admin = false. // platform admin. Anonymous callers pass user_id = uuid.Nil, is_admin = false.
@@ -170,8 +164,6 @@ type Querier interface {
ListServiceMetrics(ctx context.Context, arg ListServiceMetricsParams) ([]ListServiceMetricsRow, error) ListServiceMetrics(ctx context.Context, arg ListServiceMetricsParams) ([]ListServiceMetricsRow, error)
ListServiceTemplatesByCategoryForUser(ctx context.Context, arg ListServiceTemplatesByCategoryForUserParams) ([]ServiceTemplate, error) ListServiceTemplatesByCategoryForUser(ctx context.Context, arg ListServiceTemplatesByCategoryForUserParams) ([]ServiceTemplate, error)
ListServiceTemplatesForUser(ctx context.Context, ownerID uuid.NullUUID) ([]ServiceTemplate, error) ListServiceTemplatesForUser(ctx context.Context, ownerID uuid.NullUUID) ([]ServiceTemplate, error)
ListServiceVariableValues(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariableValuesRow, error)
ListServiceVariablesWithSecret(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariablesWithSecretRow, error)
ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error) ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error)
ListUserInvites(ctx context.Context) ([]UserInvite, error) ListUserInvites(ctx context.Context) ([]UserInvite, error)
ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error) ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error)
@@ -207,8 +199,6 @@ type Querier interface {
SetProjectApproved(ctx context.Context, arg SetProjectApprovedParams) (int64, error) SetProjectApproved(ctx context.Context, arg SetProjectApprovedParams) (int64, error)
SetServiceDomainDefault(ctx context.Context, arg SetServiceDomainDefaultParams) (int64, error) SetServiceDomainDefault(ctx context.Context, arg SetServiceDomainDefaultParams) (int64, error)
SetServiceLegacyDomain(ctx context.Context, arg SetServiceLegacyDomainParams) error SetServiceLegacyDomain(ctx context.Context, arg SetServiceLegacyDomainParams) error
SetServicePublishedPort(ctx context.Context, arg SetServicePublishedPortParams) error
SetServiceReplicas(ctx context.Context, arg SetServiceReplicasParams) error
SetServiceStatus(ctx context.Context, arg SetServiceStatusParams) error SetServiceStatus(ctx context.Context, arg SetServiceStatusParams) error
SyncDeploymentProgress(ctx context.Context, arg SyncDeploymentProgressParams) error SyncDeploymentProgress(ctx context.Context, arg SyncDeploymentProgressParams) error
SyncPreviewStatuses(ctx context.Context) error SyncPreviewStatuses(ctx context.Context) error
@@ -1,289 +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 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 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 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
}
-47
View File
@@ -1,47 +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;