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
85 changed files with 2178 additions and 8691 deletions

No files matched your search

-3
View File
@@ -124,6 +124,3 @@ ref/
.playwright-mcp/ .playwright-mcp/
canvas-*.png canvas-*.png
.vercel .vercel
# Local design previews (not shipped)
design/
+4 -14
View File
@@ -1,6 +1,6 @@
# Containr — Roadmap 2: The dflow Convergence # Containr — Roadmap 2: The dflow Convergence
Last verified: **2026-10-10** — deep pass over the full dflow codebase Last verified: **2026-10-08** — deep pass over the full dflow codebase
(`~/Desktop/PROG+HTML/dflow`, community fork of dflow-sh/dflow, frozen for (`~/Desktop/PROG+HTML/dflow`, community fork of dflow-sh/dflow, frozen for
preservation) against Containr `main` (`15b5b0e`). preservation) against Containr `main` (`15b5b0e`).
@@ -337,9 +337,8 @@ and wired refs, one click or one CLI call.
and local + tags are rejected, empty array clears. Surfaces: and local + tags are rejected, empty array clears. Surfaces:
`--placement-tags` on services create/update, `--tags` on `--placement-tags` on services create/update, `--tags` on
`nodes update`, MCP args, OpenAPI, node-detail tag editor + `nodes update`, MCP args, OpenAPI, node-detail tag editor +
service-detail placement-tags input. Build-on-node and remote service-detail placement-tags input. Remaining: build-on-node,
sleep/wake both shipped (see below). Remaining: spread weighting remote sleep/wake, spread weighting by real capacity.
by real capacity.
- [x] **Agent self-upgrade** — `POST /agents/:id/upgrade` enqueues a - [x] **Agent self-upgrade** — `POST /agents/:id/upgrade` enqueues a
`self_upgrade` command carrying platform + sha256 + server version; `self_upgrade` command carrying platform + sha256 + server version;
the agent downloads its replacement from `GET /agents/download/ the agent downloads its replacement from `GET /agents/download/
@@ -538,18 +537,9 @@ 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.
- [x] **sqlc port** — all static `database/sql` call sites → `sqlc/queries/` + generated `sqlcdb`. Merged: metrics + domains (#75), service clone/move (#76), preview environments (#77), deployments + rollback (#78), runtime ops (#79), cron (#80), sleep/wake sweeper (#81), git providers/repos/webhooks (#82), auth + security (#83), operations/settings/audit/activity/builds/misc + apwhy gateway (#84). Raw SQL remains only for runtime-assembled statements (filtered lists, sparse updates) and Better Auth `auth_users` probes. - [ ] **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] **UI/UX audit** — browser-driven pass over the whole app (PR #85):
fixed the stuck connectivity banner (`pingServer` returned
`undefined`), stale persisted sessions, fabricated observability
metrics (now real `getServiceMetrics` aggregation), mobile overflow,
Security blank state, deep-linkable service tabs, and a dozen
polish items.
- [x] **Design previews** — gitignored `/design` holds three on-brand
HTML concepts (console, service detail, landing) on the product's
token system with a live white-label accent switcher.
- [x] **First-run wizard** — public `GET /setup/status` + authed - [x] **First-run wizard** — public `GET /setup/status` + authed
`POST /setup/complete`; `/setup` page steps admin account → GitHub App `POST /setup/complete`; `/setup` page steps admin account → GitHub App
manifest flow (callback returns to the wizard via a session marker) → manifest flow (callback returns to the wizard via a session marker) →
+30 -21
View File
@@ -12,7 +12,6 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
@@ -181,13 +180,13 @@ func handleGetProjectActivity(c *gin.Context) {
} }
// Access check: approved / owner / member / admin. // Access check: approved / owner / member / admin.
userUUID, _ := uuid.Parse(userID) var visible bool
visible, err := sqlcdb.New(db.DB).CheckSecurityProjectAccess(context.Background(), sqlcdb.CheckSecurityProjectAccessParams{ if err := db.QueryRow(
ID: uuid.MustParse(projectID), `SELECT p.is_approved OR p.owner_id = $2::uuid OR $3::bool
OwnerID: userUUID, OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $2::uuid)
Column3: contextIsAdmin(c), FROM projects p WHERE p.id = $1`,
}) projectID, userID, contextIsAdmin(c),
if err != nil || !visible { ).Scan(&visible); err != nil || !visible {
respondError(c, http.StatusNotFound, "NOT_FOUND", "project not found") respondError(c, http.StatusNotFound, "NOT_FOUND", "project not found")
return return
} }
@@ -238,12 +237,12 @@ func StartAuditRetention(ctx context.Context, db *database.DB) {
} }
prune := func() { prune := func() {
n, err := sqlcdb.New(db.DB).PruneAuditLogs(context.Background(), int32(days)) res, err := db.Exec(`DELETE FROM audit_logs WHERE created_at < NOW() - ($1::int * INTERVAL '1 day')`, days)
if err != nil { if err != nil {
log.Printf("audit retention prune failed: %v", err) log.Printf("audit retention prune failed: %v", err)
return return
} }
if n > 0 { if n, _ := res.RowsAffected(); n > 0 {
log.Printf("audit retention: pruned %d rows older than %d days", n, days) log.Printf("audit retention: pruned %d rows older than %d days", n, days)
} }
} }
@@ -266,24 +265,34 @@ func StartAuditRetention(ctx context.Context, db *database.DB) {
// backfillAuditMetadata classifies rows written before severity/category/label // backfillAuditMetadata classifies rows written before severity/category/label
// existed so filters and the feed see uniform metadata. // existed so filters and the feed see uniform metadata.
func backfillAuditMetadata(db *database.DB) { func backfillAuditMetadata(db *database.DB) {
q := sqlcdb.New(db.DB) cursor := uuid.Nil.String()
cursor := uuid.Nil
for { for {
rows, err := q.ListAuditBackfillBatch(context.Background(), cursor) rows, err := db.Query(
`SELECT id::text, resource, action FROM audit_logs
WHERE label = '' AND id > $1::uuid ORDER BY id LIMIT 500`, cursor)
if err != nil { if err != nil {
log.Printf("audit backfill scan failed: %v", err) log.Printf("audit backfill scan failed: %v", err)
return return
} }
if len(rows) == 0 { type row struct{ id, resource, action string }
var batch []row
for rows.Next() {
var r row
if rows.Scan(&r.id, &r.resource, &r.action) == nil {
batch = append(batch, r)
}
}
rows.Close()
if len(batch) == 0 {
return return
} }
for _, r := range rows { for _, r := range batch {
id, _ := uuid.Parse(r.ID) cursor = r.id
cursor = id sev, cat, label := classifyAuditEvent(r.resource, r.action)
sev, cat, label := classifyAuditEvent(r.Resource, r.Action) if _, err := db.Exec(
if err := q.SetAuditLogMetadata(context.Background(), sqlcdb.SetAuditLogMetadataParams{ `UPDATE audit_logs SET severity = $1, category = $2, label = $3 WHERE id = $4::uuid`,
Severity: sev, Category: cat, Label: label, Column4: id, sev, cat, label, r.id,
}); err != nil { ); err != nil {
log.Printf("audit backfill update failed: %v", err) log.Printf("audit backfill update failed: %v", err)
} }
} }
+34 -23
View File
@@ -52,11 +52,15 @@ func EnsureAdminAccount(ctx context.Context, db *database.DB, cfg *config.Config
return return
} }
if _, err := sqlcdb.New(db.DB).UpsertAdminUser(ctx, sqlcdb.UpsertAdminUserParams{ var userID string
Email: email, err = db.QueryRowContext(ctx, `
PasswordHash: string(hashed), INSERT INTO users (email, password_hash, name, is_admin)
Name: name, VALUES ($1, $2, $3, true)
}); err != nil { ON CONFLICT (email) DO UPDATE
SET is_admin = true, updated_at = NOW()
RETURNING id
`, email, string(hashed), name).Scan(&userID)
if err != nil {
log.Printf("Admin bootstrap failed to upsert user %s: %v", email, err) log.Printf("Admin bootstrap failed to upsert user %s: %v", email, err)
return return
} }
@@ -181,18 +185,24 @@ func handleAdminOverview(c *gin.Context) {
func handleAdminListUsers(c *gin.Context) { func handleAdminListUsers(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
rows, err := sqlcdb.New(db.DB).ListAdminUsers(c.Request.Context()) rows, err := db.QueryContext(c.Request.Context(), `
SELECT id, email, name, COALESCE(avatar_url, ''), is_admin, COALESCE(to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"'), '')
FROM users
ORDER BY created_at ASC
`)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list users"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list users"})
return return
} }
defer rows.Close()
users := make([]adminUserRow, 0, len(rows)) users := make([]adminUserRow, 0)
for _, r := range rows { for rows.Next() {
users = append(users, adminUserRow{ var u adminUserRow
ID: r.ID.String(), Email: r.Email, Name: r.Name, if err := rows.Scan(&u.ID, &u.Email, &u.Name, &u.AvatarURL, &u.IsAdmin, &u.CreatedAt); err != nil {
AvatarURL: r.AvatarUrl, IsAdmin: r.IsAdmin, CreatedAt: r.CreatedAt, continue
}) }
users = append(users, u)
} }
c.JSON(http.StatusOK, gin.H{"users": users}) c.JSON(http.StatusOK, gin.H{"users": users})
} }
@@ -219,16 +229,15 @@ func handleAdminSetUserAdmin(c *gin.Context) {
return return
} }
targetUUID, _ := uuid.Parse(targetID) result, err := db.ExecContext(c.Request.Context(),
n, err := sqlcdb.New(db.DB).SetUserAdmin(c.Request.Context(), sqlcdb.SetUserAdminParams{ `UPDATE users SET is_admin = $1, updated_at = NOW() WHERE id = $2`,
IsAdmin: req.IsAdmin, req.IsAdmin, targetID,
ID: targetUUID, )
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
return return
} }
if n == 0 { if n, _ := result.RowsAffected(); n == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return return
} }
@@ -249,13 +258,15 @@ func handleAdminImpersonateUser(c *gin.Context) {
return return
} }
userUUID, _ := uuid.Parse(targetID) var email, name string
info, err := sqlcdb.New(db.DB).GetAdminUserInfo(c.Request.Context(), userUUID) var isAdmin bool
err := db.QueryRowContext(c.Request.Context(),
`SELECT email, COALESCE(name, ''), is_admin FROM users WHERE id = $1`, targetID).
Scan(&email, &name, &isAdmin)
if err != nil { if err != nil {
respondError(c, http.StatusNotFound, "NOT_FOUND", "user not found") respondError(c, http.StatusNotFound, "NOT_FOUND", "user not found")
return return
} }
email, name, isAdmin := info.Email, info.Name, info.IsAdmin
if isAdmin { if isAdmin {
respondError(c, http.StatusForbidden, "FORBIDDEN", "admin accounts cannot be impersonated") respondError(c, http.StatusForbidden, "FORBIDDEN", "admin accounts cannot be impersonated")
return return
@@ -275,9 +286,9 @@ func handleAdminImpersonateUser(c *gin.Context) {
} }
LogAuditWithRequest(c, "user", targetID, "impersonate", map[string]interface{}{ LogAuditWithRequest(c, "user", targetID, "impersonate", map[string]interface{}{
"actor_id": actorID, "actor_id": actorID,
"target_email": email, "target_email": email,
"expires_at": expiresAt.Format(time.RFC3339), "expires_at": expiresAt.Format(time.RFC3339),
}) })
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"token": token, "token": token,
+189 -121
View File
@@ -2,7 +2,6 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"context" "context"
"crypto/rand" "crypto/rand"
"database/sql" "database/sql"
@@ -15,7 +14,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/lib/pq" "github.com/lib/pq"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -162,22 +160,21 @@ func handleAPwhyServiceValidate(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
ctx := c.Request.Context() ctx := c.Request.Context()
svcUUID, parseErr := uuid.Parse(serviceID) var upstreamURL, healthPath string
var svc sqlcdb.GetAPServiceForValidateRow var authHeader, authValue sql.NullString
var err error var timeoutMs sql.NullInt64
if parseErr == nil { err := db.QueryRowContext(ctx, `
svc, err = sqlcdb.New(db.DB).GetAPServiceForValidate(ctx, svcUUID) SELECT upstream_url, health_path, upstream_auth_header, upstream_auth_value, request_timeout_ms
} FROM api_services WHERE id = $1
if parseErr != nil || err != nil { `, serviceID).Scan(&upstreamURL, &healthPath, &authHeader, &authValue, &timeoutMs)
if err != nil {
sendErrorResponse(c, http.StatusNotFound, "SERVICE_NOT_FOUND", "Service not found", nil) sendErrorResponse(c, http.StatusNotFound, "SERVICE_NOT_FOUND", "Service not found", nil)
return return
} }
upstreamURL, healthPath := svc.UpstreamUrl, svc.HealthPath
authHeader, authValue := svc.UpstreamAuthHeader, svc.UpstreamAuthValue
timeout := 8000 timeout := 8000
if svc.RequestTimeoutMs.Valid && svc.RequestTimeoutMs.Int32 > 0 { if timeoutMs.Valid && timeoutMs.Int64 > 0 {
timeout = int(svc.RequestTimeoutMs.Int32) timeout = int(timeoutMs.Int64)
} }
target := strings.TrimRight(upstreamURL, "/") + "/" + strings.TrimLeft(healthPath, "/") target := strings.TrimRight(upstreamURL, "/") + "/" + strings.TrimLeft(healthPath, "/")
@@ -206,19 +203,17 @@ func handleAPwhyServiceValidate(c *gin.Context) {
validationStatus = "healthy" validationStatus = "healthy"
} }
q := sqlcdb.New(db.DB) _, _ = db.ExecContext(ctx, `
_ = q.SetAPServiceValidation(ctx, sqlcdb.SetAPServiceValidationParams{ UPDATE api_services
LastValidationStatus: sql.NullString{String: validationStatus, Valid: true}, SET last_validation_at = NOW(), last_validation_status = $1, last_validation_message = $2, updated_at = NOW()
LastValidationMessage: sql.NullString{String: message, Valid: true}, WHERE id = $3
ID: svcUUID, `, validationStatus, message, serviceID)
})
if !ok { if !ok {
_ = q.InsertServiceIncident(ctx, sqlcdb.InsertServiceIncidentParams{ _, _ = db.ExecContext(ctx, `
ServiceID: uuid.NullUUID{UUID: svcUUID, Valid: true}, INSERT INTO incident_events (service_id, code, message, severity, http_status, occurred_at)
Message: message, VALUES ($1, 'SERVICE_VALIDATION_FAILED', $2, 'medium', $3, NOW())
HttpStatus: sql.NullInt32{Int32: int32(status), Valid: true}, `, serviceID, message, status)
})
} }
sendSuccessResponse(c, http.StatusOK, map[string]interface{}{ sendSuccessResponse(c, http.StatusOK, map[string]interface{}{
@@ -236,24 +231,36 @@ func handleAPwhyServicesList(c *gin.Context) {
ctx := context.Background() ctx := context.Background()
// Query services from database // Query services from database
rows, err := sqlcdb.New(db.DB).ListAPServices(ctx) rows, err := db.QueryContext(ctx, `
SELECT id, name, slug, upstream_url, route_prefix, enabled, created_at, updated_at
FROM api_services
ORDER BY created_at DESC
`)
if err != nil { if err != nil {
sendErrorResponse(c, http.StatusInternalServerError, "DATABASE_ERROR", sendErrorResponse(c, http.StatusInternalServerError, "DATABASE_ERROR",
"Failed to query services", err.Error()) "Failed to query services", err.Error())
return return
} }
defer rows.Close()
services := make([]map[string]interface{}, 0)
for rows.Next() {
var id, name, slug, upstreamURL, routePrefix, createdAt, updatedAt string
var enabled int
err := rows.Scan(&id, &name, &slug, &upstreamURL, &routePrefix, &enabled, &createdAt, &updatedAt)
if err != nil {
continue // Skip malformed rows
}
services := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
services = append(services, map[string]interface{}{ services = append(services, map[string]interface{}{
"id": r.ID.String(), "id": id,
"name": r.Name, "name": name,
"slug": r.Slug, "slug": slug,
"upstreamUrl": r.UpstreamUrl, "upstreamUrl": upstreamURL,
"routePrefix": r.RoutePrefix, "routePrefix": routePrefix,
"enabled": r.Enabled != 0, "enabled": enabled != 0,
"createdAt": r.CreatedAt, "createdAt": createdAt,
"updatedAt": r.UpdatedAt, "updatedAt": updatedAt,
}) })
} }
@@ -292,14 +299,14 @@ func handleAPwhyServicesCreate(c *gin.Context) {
} }
// id defaults to gen_random_uuid(); enabled defaults to 1. // id defaults to gen_random_uuid(); enabled defaults to 1.
svcID, err := sqlcdb.New(db.DB).InsertAPService(ctx, sqlcdb.InsertAPServiceParams{ var id string
Name: req.Name, err := db.QueryRowContext(ctx, `
Slug: slug, INSERT INTO api_services (
UpstreamUrl: req.UpstreamURL, name, slug, upstream_url, route_prefix, health_path,
RoutePrefix: req.RoutePrefix, rpm_limit, monthly_quota, created_at, updated_at
RpmLimit: sql.NullInt32{Int32: int32(rpmLimit), Valid: true}, ) VALUES ($1, $2, $3, $4, '/health', $5, $6, NOW(), NOW())
MonthlyQuota: sql.NullInt32{Int32: int32(monthlyQuota), Valid: true}, RETURNING id
}) `, req.Name, slug, req.UpstreamURL, req.RoutePrefix, rpmLimit, monthlyQuota).Scan(&id)
if err != nil { if err != nil {
if isUniqueViolation(err) { if isUniqueViolation(err) {
@@ -313,7 +320,7 @@ func handleAPwhyServicesCreate(c *gin.Context) {
} }
serviceData := map[string]interface{}{ serviceData := map[string]interface{}{
"id": svcID.String(), "id": id,
"name": req.Name, "name": req.Name,
"slug": slug, "slug": slug,
"upstreamUrl": req.UpstreamURL, "upstreamUrl": req.UpstreamURL,
@@ -349,15 +356,13 @@ func handleAPwhyServicesPatch(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
if input.Enabled != nil { if input.Enabled != nil {
enabled := int32(0) enabled := 0
if *input.Enabled { if *input.Enabled {
enabled = 1 enabled = 1
} }
svcUUID, _ := uuid.Parse(serviceID) _, err := db.ExecContext(context.Background(),
err := sqlcdb.New(db.DB).SetAPServiceEnabled(context.Background(), sqlcdb.SetAPServiceEnabledParams{ "UPDATE api_services SET enabled = $1, updated_at = NOW() WHERE id = $2",
Enabled: enabled, enabled, serviceID)
ID: svcUUID,
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@@ -378,7 +383,11 @@ func handleAPwhyServicesPatch(c *gin.Context) {
func handleAPwhyKeysList(c *gin.Context) { func handleAPwhyKeysList(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
rows, err := sqlcdb.New(db.DB).ListAPKeys(context.Background()) rows, err := db.QueryContext(context.Background(), `
SELECT id, name, key_prefix, plan, enabled, rpm_limit, monthly_quota, created_at, updated_at
FROM api_keys
ORDER BY created_at DESC
`)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"ok": true, "ok": true,
@@ -386,24 +395,34 @@ func handleAPwhyKeysList(c *gin.Context) {
}) })
return return
} }
defer rows.Close()
keys := make([]map[string]interface{}, 0)
for rows.Next() {
var id, name, keyPrefix, plan, createdAt, updatedAt string
var enabled int
var rpmLimit, monthlyQuota sql.NullInt64
err := rows.Scan(&id, &name, &keyPrefix, &plan, &enabled, &rpmLimit, &monthlyQuota, &createdAt, &updatedAt)
if err != nil {
continue
}
keys := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
key := map[string]interface{}{ key := map[string]interface{}{
"id": r.ID.String(), "id": id,
"name": r.Name, "name": name,
"keyPrefix": r.KeyPrefix, "keyPrefix": keyPrefix,
"plan": r.Plan, "plan": plan,
"enabled": r.Enabled != 0, "enabled": enabled != 0,
"createdAt": r.CreatedAt, "createdAt": createdAt,
"updatedAt": r.UpdatedAt, "updatedAt": updatedAt,
} }
if r.RpmLimit.Valid { if rpmLimit.Valid {
key["rpmLimit"] = int64(r.RpmLimit.Int32) key["rpmLimit"] = rpmLimit.Int64
} }
if r.MonthlyQuota.Valid { if monthlyQuota.Valid {
key["monthlyQuota"] = int64(r.MonthlyQuota.Int32) key["monthlyQuota"] = monthlyQuota.Int64
} }
keys = append(keys, key) keys = append(keys, key)
@@ -462,14 +481,14 @@ func handleAPwhyKeysCreate(c *gin.Context) {
} }
// id defaults to gen_random_uuid(); enabled defaults to 1. // id defaults to gen_random_uuid(); enabled defaults to 1.
keyID, err := sqlcdb.New(db.DB).InsertAPKey(context.Background(), sqlcdb.InsertAPKeyParams{ var id string
Name: input.Name, err = db.QueryRowContext(context.Background(), `
KeyHash: keyHash, INSERT INTO api_keys (
KeyPrefix: keyPrefix, name, key_hash, key_prefix, plan, allowed_service_ids,
Plan: plan, rpm_limit, monthly_quota, created_at, updated_at
RpmLimit: sql.NullInt32{Int32: int32(rpmLimit), Valid: true}, ) VALUES ($1, $2, $3, $4, '[]', $5, $6, NOW(), NOW())
MonthlyQuota: sql.NullInt32{Int32: int32(monthlyQuota), Valid: true}, RETURNING id
}) `, input.Name, keyHash, keyPrefix, plan, rpmLimit, monthlyQuota).Scan(&id)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@@ -482,7 +501,7 @@ func handleAPwhyKeysCreate(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{
"ok": true, "ok": true,
"data": gin.H{ "data": gin.H{
"id": keyID.String(), "id": id,
"name": input.Name, "name": input.Name,
"plan": plan, "plan": plan,
"key": apiKey, // Only return the actual key once "key": apiKey, // Only return the actual key once
@@ -513,15 +532,13 @@ func handleAPwhyKeysPatch(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
if input.Enabled != nil { if input.Enabled != nil {
enabled := int32(0) enabled := 0
if *input.Enabled { if *input.Enabled {
enabled = 1 enabled = 1
} }
keyUUID, _ := uuid.Parse(keyID) _, err := db.ExecContext(context.Background(),
err := sqlcdb.New(db.DB).SetAPKeyEnabled(context.Background(), sqlcdb.SetAPKeyEnabledParams{ "UPDATE api_keys SET enabled = $1, updated_at = NOW() WHERE id = $2",
Enabled: enabled, enabled, keyID)
ID: keyUUID,
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@@ -543,27 +560,23 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
// Get counts from database // Get counts from database
q := sqlcdb.New(db.DB) var totalServices, totalKeys, totalUsers int
ctx := context.Background()
totalServices, err := q.CountAPServices(ctx) if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM api_services").Scan(&totalServices); err != nil {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"ok": false, "ok": false,
"error": "Failed to count services: " + err.Error(), "error": "Failed to count services: " + err.Error(),
}) })
return return
} }
totalKeys, err := q.CountAPKeys(ctx) if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM api_keys").Scan(&totalKeys); err != nil {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"ok": false, "ok": false,
"error": "Failed to count api keys: " + err.Error(), "error": "Failed to count api keys: " + err.Error(),
}) })
return return
} }
totalUsers, err := q.CountUsers(ctx) if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM users").Scan(&totalUsers); err != nil {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"ok": false, "ok": false,
"error": "Failed to count users: " + err.Error(), "error": "Failed to count users: " + err.Error(),
@@ -571,8 +584,8 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
return return
} }
totalRequests, err := q.SumUsageCounters(ctx) var totalRequests int
if err != nil { if err := db.QueryRowContext(context.Background(), "SELECT COALESCE(SUM(request_count), 0) FROM usage_counters").Scan(&totalRequests); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"ok": false, "ok": false,
"error": "Failed to aggregate request counters: " + err.Error(), "error": "Failed to aggregate request counters: " + err.Error(),
@@ -580,8 +593,12 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
return return
} }
requestsToday, err := q.SumRequestMetricSince(ctx, "day") var requestsToday int
if err != nil { if err := db.QueryRowContext(context.Background(), `
SELECT COALESCE(SUM(value), 0)::int
FROM metrics_timeseries
WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC('day', NOW())
`).Scan(&requestsToday); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"ok": false, "ok": false,
"error": "Failed to aggregate today's requests: " + err.Error(), "error": "Failed to aggregate today's requests: " + err.Error(),
@@ -589,8 +606,12 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
return return
} }
requestsThisMonth, err := q.SumRequestMetricSince(ctx, "month") var requestsThisMonth int
if err != nil { if err := db.QueryRowContext(context.Background(), `
SELECT COALESCE(SUM(value), 0)::int
FROM metrics_timeseries
WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC('month', NOW())
`).Scan(&requestsThisMonth); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"ok": false, "ok": false,
"error": "Failed to aggregate monthly requests: " + err.Error(), "error": "Failed to aggregate monthly requests: " + err.Error(),
@@ -615,47 +636,94 @@ func handleAPwhyAnalyticsOps(c *gin.Context) {
func handleAPwhyAnalyticsTraffic(c *gin.Context) { func handleAPwhyAnalyticsTraffic(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
q := sqlcdb.New(db.DB)
ctx := context.Background()
topServices := make([]map[string]interface{}, 0) topServices := make([]map[string]interface{}, 0)
if serviceRows, err := q.ListTopAPServices(ctx); err == nil { serviceRows, err := db.QueryContext(context.Background(), `
for _, r := range serviceRows { SELECT s.id, s.name, COALESCE(SUM(u.request_count), 0) AS total_requests
topServices = append(topServices, map[string]interface{}{ FROM api_services s
"service_id": r.ID.String(), LEFT JOIN usage_counters u ON u.service_id = s.id
"name": r.Name, GROUP BY s.id, s.name
"requests": r.TotalRequests, ORDER BY total_requests DESC, s.name ASC
}) LIMIT 10
`)
if err == nil {
defer serviceRows.Close()
for serviceRows.Next() {
var serviceID, serviceName string
var totalRequests int
if scanErr := serviceRows.Scan(&serviceID, &serviceName, &totalRequests); scanErr == nil {
topServices = append(topServices, map[string]interface{}{
"service_id": serviceID,
"name": serviceName,
"requests": totalRequests,
})
}
} }
} }
requestsByDay := make([]map[string]interface{}, 0) requestsByDay := make([]map[string]interface{}, 0)
if trafficRows, err := q.ListRequestsByDay(ctx); err == nil { trafficRows, err := db.QueryContext(context.Background(), `
for _, r := range trafficRows { SELECT TO_CHAR(DATE_TRUNC('day', occurred_at), 'YYYY-MM-DD') AS day_bucket, COUNT(*) AS total
requestsByDay = append(requestsByDay, map[string]interface{}{ FROM metrics_timeseries
"day": r.DayBucket, WHERE metric = 'request_total' AND occurred_at >= NOW() - INTERVAL '7 days'
"requests": r.Total, GROUP BY DATE_TRUNC('day', occurred_at)
}) ORDER BY DATE_TRUNC('day', occurred_at) ASC
`)
if err == nil {
defer trafficRows.Close()
for trafficRows.Next() {
var day string
var count int
if scanErr := trafficRows.Scan(&day, &count); scanErr == nil {
requestsByDay = append(requestsByDay, map[string]interface{}{
"day": day,
"requests": count,
})
}
} }
} }
statusCodes := make([]map[string]interface{}, 0) statusCodes := make([]map[string]interface{}, 0)
if statusRows, err := q.ListIncidentStatusCodes(ctx); err == nil { statusRows, err := db.QueryContext(context.Background(), `
for _, r := range statusRows { SELECT COALESCE(http_status, 0) AS status_code, COALESCE(SUM(count), 0) AS total
statusCodes = append(statusCodes, map[string]interface{}{ FROM incident_events
"status_code": r.StatusCode, WHERE occurred_at >= NOW() - INTERVAL '7 days'
"count": r.Total, GROUP BY COALESCE(http_status, 0)
}) ORDER BY total DESC, status_code ASC
`)
if err == nil {
defer statusRows.Close()
for statusRows.Next() {
var code int
var total int
if scanErr := statusRows.Scan(&code, &total); scanErr == nil {
statusCodes = append(statusCodes, map[string]interface{}{
"status_code": code,
"count": total,
})
}
} }
} }
clientEvents := make([]map[string]interface{}, 0) clientEvents := make([]map[string]interface{}, 0)
if eventRows, err := q.ListClientEventPaths(ctx); err == nil { eventRows, err := db.QueryContext(context.Background(), `
for _, r := range eventRows { SELECT COALESCE((labels_json::jsonb ->> 'path'), 'unknown') AS path, COUNT(*) AS total
clientEvents = append(clientEvents, map[string]interface{}{ FROM metrics_timeseries
"path": r.Path, WHERE metric = 'client_event' AND occurred_at >= NOW() - INTERVAL '7 days'
"count": r.Total, GROUP BY COALESCE((labels_json::jsonb ->> 'path'), 'unknown')
}) ORDER BY total DESC, path ASC
LIMIT 20
`)
if err == nil {
defer eventRows.Close()
for eventRows.Next() {
var path string
var total int
if scanErr := eventRows.Scan(&path, &total); scanErr == nil {
clientEvents = append(clientEvents, map[string]interface{}{
"path": path,
"count": total,
})
}
} }
} }
+34 -54
View File
@@ -2,9 +2,6 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"context"
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@@ -14,7 +11,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
) )
type AuditLog struct { type AuditLog struct {
@@ -102,20 +98,16 @@ func LogAudit(userID, resource, resourceID, action string, details map[string]in
} }
detailsJSON, _ := json.Marshal(details) detailsJSON, _ := json.Marshal(details)
resourceUUID := parseUUIDOrNil(resourceID)
userUUID := parseUUIDOrNil(userID)
severity, category, label := classifyAuditEvent(resource, action) severity, category, label := classifyAuditEvent(resource, action)
err := sqlcdb.New(db.DB).InsertAuditLog(context.Background(), sqlcdb.InsertAuditLogParams{ auditID := uuid.New().String()
ID: uuid.New(), _, err := db.Exec(
UserID: nullUUIDOrNil(userID), `INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, severity, category, label, created_at)
Resource: resource, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
ResourceID: nullUUIDOrNil(resourceID), auditID, userUUID, resource, resourceUUID, action, string(detailsJSON), severity, category, label, time.Now().UTC(),
Action: action, )
Details: pqtype.NullRawMessage{RawMessage: detailsJSON, Valid: true},
Severity: severity,
Category: category,
Label: label,
CreatedAt: time.Now().UTC(),
})
if err != nil { if err != nil {
} }
@@ -139,24 +131,16 @@ func LogAuditWithRequest(c *gin.Context, resource, resourceID, action string, de
if uid, ok := userID.(string); ok { if uid, ok := userID.(string); ok {
userIDStr = uid userIDStr = uid
} }
userUUID := parseUUIDOrNil(userIDStr)
resourceUUID := parseUUIDOrNil(resourceID)
severity, category, label := classifyAuditEvent(resource, action) severity, category, label := classifyAuditEvent(resource, action)
var inet pqtype.Inet auditID := uuid.New().String()
_ = inet.Scan(c.ClientIP()) _, err := db.Exec(
err := sqlcdb.New(db.DB).InsertAuditLogWithRequest(context.Background(), sqlcdb.InsertAuditLogWithRequestParams{ `INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, ip_address, user_agent, severity, category, label, created_at)
ID: uuid.New(), VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9, $10, $11, $12)`,
UserID: nullUUIDOrNil(userIDStr), auditID, userUUID, resource, resourceUUID, action, string(detailsJSON), c.ClientIP(), c.GetHeader("User-Agent"), severity, category, label, time.Now().UTC(),
Resource: resource, )
ResourceID: nullUUIDOrNil(resourceID),
Action: action,
Column6: json.RawMessage(detailsJSON),
Column7: inet,
UserAgent: sql.NullString{String: c.GetHeader("User-Agent"), Valid: true},
Severity: severity,
Category: category,
Label: label,
CreatedAt: time.Now().UTC(),
})
if err != nil { if err != nil {
} }
@@ -271,29 +255,30 @@ func handleGetResourceAuditLogs(c *gin.Context) {
resource := c.Param("resource") resource := c.Param("resource")
resourceID := c.Param("id") resourceID := c.Param("id")
rows, err := sqlcdb.New(db.DB).ListResourceAuditLogs(context.Background(), sqlcdb.ListResourceAuditLogsParams{ rows, err := db.Query(
UserID: nullUUIDOrNil(userID), `SELECT id, COALESCE(user_id::text, ''), resource, COALESCE(resource_id::text, ''), action, COALESCE(details::text, '{}'),
Resource: resource, COALESCE(ip_address::text, ''), COALESCE(user_agent, ''), created_at
ResourceID: nullUUIDOrNil(resourceID), FROM audit_logs
}) WHERE user_id::text = $1 AND resource = $2 AND resource_id::text = $3
ORDER BY created_at DESC
LIMIT 100`,
userID, resource, resourceID,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch audit logs"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch audit logs"})
return return
} }
defer rows.Close()
var logs []AuditLog var logs []AuditLog
for _, row := range rows { for rows.Next() {
logs = append(logs, AuditLog{ var log AuditLog
ID: row.ID.String(), err := rows.Scan(&log.ID, &log.UserID, &log.Resource, &log.ResourceID, &log.Action, &log.Details, &log.IPAddress, &log.UserAgent, &log.CreatedAt)
UserID: row.UserID, if err != nil {
Resource: row.Resource, continue
ResourceID: row.ResourceID, }
Action: row.Action, logs = append(logs, log)
Details: row.Details,
IPAddress: row.IpAddress,
UserAgent: row.UserAgent,
CreatedAt: row.CreatedAt,
})
} }
c.JSON(http.StatusOK, gin.H{"audit_logs": logs}) c.JSON(http.StatusOK, gin.H{"audit_logs": logs})
@@ -317,8 +302,3 @@ func parseUUIDOrNil(raw string) interface{} {
} }
return trimmed return trimmed
} }
func nullUUIDOrNil(raw string) uuid.NullUUID {
parsed, err := uuid.Parse(strings.TrimSpace(raw))
return uuid.NullUUID{UUID: parsed, Valid: err == nil}
}
+46 -78
View File
@@ -3,8 +3,6 @@ package api
import ( import (
"bytes" "bytes"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -65,16 +63,11 @@ func handleLogin(c *gin.Context) {
// Find user by email // Find user by email
var user User var user User
var hashedPassword string var hashedPassword string
urow, err := sqlcdb.New(db.DB).GetUserByEmailForAuth(context.Background(), req.Email) err := db.QueryRow(`
user = User{ SELECT id, email, password_hash, name, COALESCE(avatar_url, ''), is_admin, created_at
ID: urow.ID.String(), FROM users
Email: urow.Email, WHERE email = $1
Name: urow.Name, `, req.Email).Scan(&user.ID, &user.Email, &hashedPassword, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
hashedPassword = urow.PasswordHash
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
authUser, ok := verifyBetterAuthCredentials(c, req.Email, req.Password) authUser, ok := verifyBetterAuthCredentials(c, req.Email, req.Password)
@@ -264,30 +257,25 @@ func createLocalUserFromBetterAuth(db *database.DB, authUser betterAuthLoginUser
} }
// A mirrored first account owns the platform, same as handleRegister. // A mirrored first account owns the platform, same as handleRegister.
q := sqlcdb.New(db.DB) var total int
total64, err := q.CountUsers(context.Background()) if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&total); err != nil {
if err != nil {
return User{}, err return User{}, err
} }
urow, err := q.UpsertLocalUser(context.Background(), sqlcdb.UpsertLocalUserParams{ var user User
Email: email, err = db.QueryRow(`
PasswordHash: string(hashedPassword), INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
Name: name, VALUES ($1, $2, $3, NULLIF($4, ''), $5)
Column4: strings.TrimSpace(authUser.Image), ON CONFLICT (email) DO UPDATE
IsAdmin: total64 == 0, SET name = EXCLUDED.name,
}) avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = NOW()
RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
`, email, string(hashedPassword), name, strings.TrimSpace(authUser.Image), total == 0).
Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
if err != nil { if err != nil {
return User{}, err return User{}, err
} }
user := User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
return user, nil return user, nil
} }
@@ -314,8 +302,8 @@ func handleRegister(c *gin.Context) {
return return
} }
q := sqlcdb.New(db.DB) var existing int
existing, err := q.CountUsersByEmail(context.Background(), strings.ToLower(strings.TrimSpace(req.Email))) err = db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", strings.ToLower(strings.TrimSpace(req.Email))).Scan(&existing)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
@@ -334,20 +322,12 @@ func handleRegister(c *gin.Context) {
} }
// First account owns the platform. // First account owns the platform.
urow, err := q.InsertUserAdmin(context.Background(), sqlcdb.InsertUserAdminParams{ var user User
Email: req.Email, err = db.QueryRow(`
PasswordHash: string(hashedPassword), INSERT INTO users (email, password_hash, name, is_admin)
Name: req.Name, VALUES ($1, $2, $3, $4)
IsAdmin: total == 0, RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
}) `, req.Email, string(hashedPassword), req.Name, total == 0).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
user := User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
@@ -385,8 +365,8 @@ func handleCreateUser(c *gin.Context) {
return return
} }
existing, err := sqlcdb.New(db.DB).CountUsersByEmail(context.Background(), req.Email) var existing int
if err != nil { if err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", req.Email).Scan(&existing); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
@@ -410,19 +390,12 @@ func handleCreateUser(c *gin.Context) {
return return
} }
urow, err := sqlcdb.New(db.DB).InsertUser(context.Background(), sqlcdb.InsertUserParams{ var user User
Email: req.Email, err = db.QueryRow(`
PasswordHash: string(hashedPassword), INSERT INTO users (email, password_hash, name)
Name: req.Name, VALUES ($1, $2, $3)
}) RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
user := User{ `, req.Email, string(hashedPassword), req.Name).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err != nil { if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "User already exists"}) c.JSON(http.StatusConflict, gin.H{"error": "User already exists"})
return return
@@ -485,11 +458,10 @@ func createBetterAuthUser(c *gin.Context, name string, email string, password st
} }
func countLocalUsers(db *database.DB) (int, error) { func countLocalUsers(db *database.DB) (int, error) {
count64, err := sqlcdb.New(db.DB).CountUsers(context.Background()) var count int
if err != nil { if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
return 0, err return 0, err
} }
count := int(count64)
// Better Auth writes to auth_users; a row there only mirrors into users on // Better Auth writes to auth_users; a row there only mirrors into users on
// the first authenticated call. Count it too or a second public sign-up // the first authenticated call. Count it too or a second public sign-up
// slips through the bootstrap window. UNION by email avoids double-counting // slips through the bootstrap window. UNION by email avoids double-counting
@@ -517,15 +489,11 @@ func handleGetProfile(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
var user User var user User
urow, err := sqlcdb.New(db.DB).GetUserByID(context.Background(), uuid.MustParse(userID)) err := db.QueryRow(`
user = User{ SELECT id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
ID: urow.ID.String(), FROM users
Email: urow.Email, WHERE id = $1
Name: urow.Name, `, userID).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
@@ -553,11 +521,11 @@ func handleUpdateProfile(c *gin.Context) {
} }
// Update user profile // Update user profile
err := sqlcdb.New(db.DB).UpdateUserProfile(context.Background(), sqlcdb.UpdateUserProfileParams{ _, err := db.Exec(`
Column1: req.Name, UPDATE users
Column2: req.AvatarURL, SET name = COALESCE($1, name), avatar_url = COALESCE($2, avatar_url)
ID: uuid.MustParse(userID), WHERE id = $3
}) `, req.Name, req.AvatarURL, userID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"})
+65 -55
View File
@@ -16,7 +16,6 @@ import (
"containr/internal/build" "containr/internal/build"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/docker" "containr/internal/docker"
"containr/internal/types" "containr/internal/types"
@@ -670,22 +669,39 @@ func (h *BuildHandler) upsertBuild(status *BuildStatusResponse) error {
} }
now := time.Now().UTC() now := time.Now().UTC()
err := sqlcdb.New(h.db.DB).UpsertBuild(context.Background(), sqlcdb.UpsertBuildParams{ _, err := h.db.Exec(
ID: status.ID, `INSERT INTO builds
ProjectID: nsNullIfEmpty(status.ProjectID), (id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata, created_at, updated_at)
ServiceID: nsNullIfEmpty(status.ServiceID), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $14)
Status: status.Status, ON CONFLICT (id) DO UPDATE SET
Progress: int32(status.Progress), project_id = EXCLUDED.project_id,
StartedAt: status.StartedAt, service_id = EXCLUDED.service_id,
CompletedAt: ntPtr(status.CompletedAt), status = EXCLUDED.status,
ImageName: status.ImageName, progress = EXCLUDED.progress,
ImageTag: status.ImageTag, started_at = EXCLUDED.started_at,
Size: status.Size, completed_at = EXCLUDED.completed_at,
Error: nsNullIfEmpty(status.Error), image_name = EXCLUDED.image_name,
Log: status.Log, image_tag = EXCLUDED.image_tag,
Column13: json.RawMessage(jsonOrEmptyObject(metadataRaw)), size = EXCLUDED.size,
CreatedAt: now, error = EXCLUDED.error,
}) log = EXCLUDED.log,
metadata = EXCLUDED.metadata,
updated_at = EXCLUDED.updated_at`,
status.ID,
nullIfEmptyString(status.ProjectID),
nullIfEmptyString(status.ServiceID),
status.Status,
status.Progress,
status.StartedAt,
status.CompletedAt,
status.ImageName,
status.ImageTag,
status.Size,
nullIfEmptyString(status.Error),
status.Log,
jsonOrEmptyObject(metadataRaw),
now,
)
if err != nil && h.isMissingBuildsTable(err) { if err != nil && h.isMissingBuildsTable(err) {
return nil return nil
} }
@@ -697,37 +713,18 @@ func (h *BuildHandler) getBuildFromDB(buildID string) (BuildStatusResponse, bool
return BuildStatusResponse{}, false, nil return BuildStatusResponse{}, false, nil
} }
row, err := sqlcdb.New(h.db.DB).GetBuild(context.Background(), buildID) row := h.db.QueryRow(
if err != nil { `SELECT id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata
if errors.Is(err, sql.ErrNoRows) || h.isMissingBuildsTable(err) { FROM builds
return BuildStatusResponse{}, false, nil WHERE id = $1`,
} buildID,
return BuildStatusResponse{}, false, err )
build, found, err := scanBuildRow(row.Scan)
if err != nil && h.isMissingBuildsTable(err) {
return BuildStatusResponse{}, false, nil
} }
build := BuildStatusResponse{ return build, found, err
ID: row.ID,
ProjectID: row.ProjectID.String,
ServiceID: row.ServiceID.String,
Status: row.Status,
Progress: int(row.Progress),
StartedAt: row.StartedAt.UTC(),
ImageName: row.ImageName,
ImageTag: row.ImageTag,
Size: row.Size,
Error: row.Error.String,
Log: row.Log,
}
if t := timePtrNT(row.CompletedAt); t != nil {
u := t.UTC()
build.CompletedAt = &u
}
if len(row.Metadata) > 0 {
metadata := map[string]string{}
if err := json.Unmarshal(row.Metadata, &metadata); err == nil {
build.Metadata = metadata
}
}
return build, true, nil
} }
func (h *BuildHandler) listBuildsFromDB(projectID, serviceID, status string, page, limit int) ([]BuildStatusResponse, int, error) { func (h *BuildHandler) listBuildsFromDB(projectID, serviceID, status string, page, limit int) ([]BuildStatusResponse, int, error) {
@@ -811,11 +808,19 @@ func (h *BuildHandler) cancelBuildInDB(buildID string) bool {
return false return false
} }
now := time.Now().UTC() now := time.Now().UTC()
affected, err := sqlcdb.New(h.db.DB).CancelBuild(context.Background(), sqlcdb.CancelBuildParams{ result, err := h.db.Exec(
CompletedAt: sql.NullTime{Time: now, Valid: true}, `UPDATE builds
Log: h.formatLogLine("Cancellation requested") + "\n", SET status = 'cancelled',
ID: buildID, progress = 100,
}) completed_at = $1,
log = COALESCE(log, '') || $2,
updated_at = $1
WHERE id = $3
AND status NOT IN ('success', 'failed', 'cancelled')`,
now,
h.formatLogLine("Cancellation requested")+"\n",
buildID,
)
if err != nil { if err != nil {
if h.isMissingBuildsTable(err) { if h.isMissingBuildsTable(err) {
return false return false
@@ -823,7 +828,9 @@ func (h *BuildHandler) cancelBuildInDB(buildID string) bool {
log.Printf("failed to cancel build %s in database: %v", buildID, err) log.Printf("failed to cancel build %s in database: %v", buildID, err)
return false return false
} }
if affected == 0 {
affected, err := result.RowsAffected()
if err != nil || affected == 0 {
return false return false
} }
@@ -902,9 +909,12 @@ func scanBuildRow(scan func(dest ...interface{}) error) (BuildStatusResponse, bo
return parsed, true, nil return parsed, true, nil
} }
func nsNullIfEmpty(value string) sql.NullString { func nullIfEmptyString(value string) interface{} {
trimmed := strings.TrimSpace(value) trimmed := strings.TrimSpace(value)
return sql.NullString{String: trimmed, Valid: trimmed != ""} if trimmed == "" {
return nil
}
return trimmed
} }
func jsonOrEmptyObject(raw []byte) string { func jsonOrEmptyObject(raw []byte) string {
+23 -18
View File
@@ -1,11 +1,9 @@
package api package api
import ( import (
"context"
"net/http" "net/http"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
@@ -59,12 +57,15 @@ func contextIsAdmin(c *gin.Context) bool {
// public; unapproved ones are visible to the owner, project members, and // public; unapproved ones are visible to the owner, project members, and
// platform admins only. // platform admins only.
func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) { func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) {
q := sqlcdb.New(db.DB) var isApproved bool
row, err := q.GetProjectReadAccess(context.Background(), projectID) var ownerID string
err := db.QueryRow(
`SELECT is_approved, owner_id::text FROM projects WHERE id = $1`,
projectID,
).Scan(&isApproved, &ownerID)
if err != nil { if err != nil {
return false, false return false, false
} }
isApproved, ownerID := row.IsApproved, row.OwnerID
if isApproved || contextIsAdmin(c) { if isApproved || contextIsAdmin(c) {
return true, true return true, true
} }
@@ -75,11 +76,11 @@ func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bo
if ownerID == userID { if ownerID == userID {
return true, true return true, true
} }
userUUID, _ := uuid.Parse(userID) var member bool
if member, err := q.ProjectMemberExists(context.Background(), sqlcdb.ProjectMemberExistsParams{ if err := db.QueryRow(
ProjectID: projectID, `SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2)`,
UserID: userUUID, projectID, userID,
}); err == nil && member { ).Scan(&member); err == nil && member {
return true, true return true, true
} }
return true, false return true, false
@@ -90,8 +91,11 @@ func projectReadAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bo
// project members, and platform admins may reach secret-adjacent data // project members, and platform admins may reach secret-adjacent data
// such as variables and runtime logs. // such as variables and runtime logs.
func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) { func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (bool, bool) {
q := sqlcdb.New(db.DB) var ownerID string
ownerID, err := q.GetProjectOwnerText(context.Background(), projectID) err := db.QueryRow(
`SELECT owner_id::text FROM projects WHERE id = $1`,
projectID,
).Scan(&ownerID)
if err != nil { if err != nil {
return false, false return false, false
} }
@@ -105,11 +109,11 @@ func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (
if ownerID == userID { if ownerID == userID {
return true, true return true, true
} }
userUUID, _ := uuid.Parse(userID) var member bool
if member, err := q.ProjectMemberExists(context.Background(), sqlcdb.ProjectMemberExistsParams{ if err := db.QueryRow(
ProjectID: projectID, `SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2)`,
UserID: userUUID, projectID, userID,
}); err == nil && member { ).Scan(&member); err == nil && member {
return true, true return true, true
} }
return true, false return true, false
@@ -117,7 +121,8 @@ func projectManageAccess(c *gin.Context, db *database.DB, projectID uuid.UUID) (
// serviceProjectID resolves the project that owns a service. // serviceProjectID resolves the project that owns a service.
func serviceProjectID(db *database.DB, serviceID uuid.UUID) (uuid.UUID, bool) { func serviceProjectID(db *database.DB, serviceID uuid.UUID) (uuid.UUID, bool) {
projectID, err := sqlcdb.New(db.DB).GetServiceProjectID(context.Background(), serviceID) var projectID uuid.UUID
err := db.QueryRow(`SELECT project_id FROM services WHERE id = $1`, serviceID).Scan(&projectID)
if err != nil { if err != nil {
return uuid.Nil, false return uuid.Nil, false
} }
+123 -147
View File
@@ -65,47 +65,13 @@ type UpdateCronJobRequest struct {
Retention int `json:"retention"` Retention int `json:"retention"`
} }
// cronJobFrom maps the sqlc row onto the string-ID API model.
func cronJobFrom(j sqlcdb.CronJob) CronJob {
return CronJob{
ID: j.ID.String(),
ProjectID: j.ProjectID.String(),
ServiceID: j.ServiceID.String(),
Name: j.Name,
Schedule: j.Schedule,
Command: j.Command,
Timezone: j.Timezone.String,
Enabled: j.Enabled.Bool,
LastRunAt: timePtrNT(j.LastRunAt),
NextRunAt: timePtrNT(j.NextRunAt),
LastStatus: j.LastStatus.String,
LastOutput: j.LastOutput.String,
Retention: int(j.Retention.Int32),
CreatedAt: j.CreatedAt.Time,
UpdatedAt: j.UpdatedAt.Time,
}
}
// cronExecutionFrom maps the sqlc row onto the string-ID API model.
func cronExecutionFrom(e sqlcdb.CronExecution) CronExecution {
return CronExecution{
ID: e.ID.String(),
CronJobID: e.CronJobID.String(),
StartedAt: e.StartedAt,
FinishedAt: timePtrNT(e.FinishedAt),
Status: e.Status.String,
Output: e.Output.String,
Error: e.Error.String,
}
}
// cronJobProjectID resolves the project a cron job belongs to. // cronJobProjectID resolves the project a cron job belongs to.
func cronJobProjectID(db *database.DB, jobID string) (uuid.UUID, bool) { func cronJobProjectID(db *database.DB, jobID string) (uuid.UUID, bool) {
id, err := uuid.Parse(jobID) var projectID uuid.UUID
if err != nil { err := db.QueryRow(
return uuid.Nil, false `SELECT cj.project_id FROM cron_jobs cj WHERE cj.id = $1`,
} jobID,
projectID, err := sqlcdb.New(db.DB).GetCronJobProjectID(context.Background(), id) ).Scan(&projectID)
if err != nil { if err != nil {
return uuid.Nil, false return uuid.Nil, false
} }
@@ -171,14 +137,15 @@ func handleCreateCronJob(c *gin.Context) {
return return
} }
svcID, err := uuid.Parse(req.ServiceID) var ownerCheck string
if err != nil { err := db.QueryRow(
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) `SELECT p.owner_id FROM projects p
return JOIN services s ON s.project_id = p.id
} WHERE s.id = $1`,
ownerCheck, err := sqlcdb.New(db.DB).GetCronOwnerViaService(context.Background(), svcID) req.ServiceID,
).Scan(&ownerCheck)
if err != nil || (ownerCheck.String() != userID && !contextIsAdmin(c)) { if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -211,20 +178,11 @@ func handleCreateCronJob(c *gin.Context) {
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
err = sqlcdb.New(db.DB).InsertCronJob(context.Background(), sqlcdb.InsertCronJobParams{ _, err = db.Exec(
ID: uuid.MustParse(job.ID), `INSERT INTO cron_jobs (id, project_id, service_id, name, schedule, command, timezone, enabled, next_run_at, retention, created_at, updated_at)
ProjectID: uuid.MustParse(job.ProjectID), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
ServiceID: uuid.MustParse(job.ServiceID), job.ID, job.ProjectID, job.ServiceID, job.Name, job.Schedule, job.Command, job.Timezone, job.Enabled, job.NextRunAt, job.Retention, job.CreatedAt, job.UpdatedAt,
Name: job.Name, )
Schedule: job.Schedule,
Command: job.Command,
Timezone: sql.NullString{String: job.Timezone, Valid: true},
Enabled: sql.NullBool{Bool: job.Enabled, Valid: true},
NextRunAt: ntPtr(job.NextRunAt),
Retention: sql.NullInt32{Int32: int32(job.Retention), Valid: true},
CreatedAt: sql.NullTime{Time: job.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: job.UpdatedAt, Valid: true},
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create cron job"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create cron job"})
@@ -243,17 +201,22 @@ func handleGetCronJob(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
jobID := c.Param("id") jobID := c.Param("id")
jid, err := uuid.Parse(jobID) var job CronJob
err := db.QueryRow(
`SELECT cj.id, cj.project_id, cj.service_id, cj.name, cj.schedule, cj.timezone,
cj.enabled, cj.last_run_at, cj.next_run_at, cj.last_status, cj.last_output,
cj.retention, cj.created_at, cj.updated_at
FROM cron_jobs cj
WHERE cj.id = $1`,
jobID,
).Scan(&job.ID, &job.ProjectID, &job.ServiceID, &job.Name, &job.Schedule, &job.Timezone,
&job.Enabled, &job.LastRunAt, &job.NextRunAt, &job.LastStatus, &job.LastOutput,
&job.Retention, &job.CreatedAt, &job.UpdatedAt)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Cron job not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Cron job not found"})
return return
} }
row, err := sqlcdb.New(db.DB).GetCronJob(context.Background(), jid)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Cron job not found"})
return
}
job := cronJobFrom(row)
projectID, found := cronJobProjectID(db, jobID) projectID, found := cronJobProjectID(db, jobID)
if !found { if !found {
@@ -279,14 +242,15 @@ func handleUpdateCronJob(c *gin.Context) {
return return
} }
jid, err := uuid.Parse(jobID) var ownerCheck string
if err != nil { err := db.QueryRow(
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) `SELECT p.owner_id FROM cron_jobs cj
return JOIN projects p ON cj.project_id = p.id
} WHERE cj.id = $1`,
ownerCheck, err := sqlcdb.New(db.DB).GetCronJobOwner(context.Background(), jid) jobID,
).Scan(&ownerCheck)
if err != nil || (ownerCheck.String() != userID && !contextIsAdmin(c)) { if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -348,19 +312,21 @@ func handleDeleteCronJob(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
jobID := c.Param("id") jobID := c.Param("id")
jid, err := uuid.Parse(jobID) var ownerCheck string
err := db.QueryRow(
`SELECT p.owner_id FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1`,
jobID,
).Scan(&ownerCheck)
if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
_, err = db.Exec("DELETE FROM cron_jobs WHERE id = $1", jobID)
if err != nil { if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
ownerCheck, err := sqlcdb.New(db.DB).GetCronJobOwner(context.Background(), jid)
if err != nil || (ownerCheck.String() != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
if err := sqlcdb.New(db.DB).DeleteCronJob(context.Background(), jid); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cron job"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cron job"})
return return
} }
@@ -384,20 +350,29 @@ func handleGetCronExecutions(c *gin.Context) {
return return
} }
jid, err := uuid.Parse(jobID) rows, err := db.Query(
if err != nil { `SELECT id, cron_job_id, started_at, finished_at, status, output, error
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"}) FROM cron_executions
return WHERE cron_job_id = $1
} ORDER BY started_at DESC
rows, err := sqlcdb.New(db.DB).ListCronExecutions(context.Background(), jid) LIMIT 100`,
jobID,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"})
return return
} }
defer rows.Close()
var executions []CronExecution var executions []CronExecution
for _, e := range rows { for rows.Next() {
executions = append(executions, cronExecutionFrom(e)) var exec CronExecution
err := rows.Scan(&exec.ID, &exec.CronJobID, &exec.StartedAt, &exec.FinishedAt, &exec.Status, &exec.Output, &exec.Error)
if err != nil {
continue
}
executions = append(executions, exec)
} }
c.JSON(http.StatusOK, gin.H{"executions": executions}) c.JSON(http.StatusOK, gin.H{"executions": executions})
@@ -408,33 +383,29 @@ func handleTriggerCronJob(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
jobID := c.Param("id") jobID := c.Param("id")
jid, err := uuid.Parse(jobID) var job CronJob
if err != nil { var ownerCheck string
err := db.QueryRow(
`SELECT cj.service_id, cj.command, cj.schedule, cj.timezone, cj.retention, p.owner_id
FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1`,
jobID,
).Scan(&job.ServiceID, &job.Command, &job.Schedule, &job.Timezone, &job.Retention, &ownerCheck)
if err != nil || (ownerCheck != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
trig, err := sqlcdb.New(db.DB).GetCronJobForTrigger(context.Background(), jid)
if err != nil || (trig.OwnerID.String() != userID && !contextIsAdmin(c)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
job := CronJob{
ServiceID: trig.ServiceID.String(),
Command: trig.Command,
Schedule: trig.Schedule,
Timezone: trig.Timezone.String,
Retention: int(trig.Retention.Int32),
}
execID := uuid.New().String() execID := uuid.New().String()
now := time.Now() now := time.Now()
_ = sqlcdb.New(db.DB).InsertCronExecution(context.Background(), sqlcdb.InsertCronExecutionParams{ _, err = db.Exec(
ID: uuid.MustParse(execID), `INSERT INTO cron_executions (id, cron_job_id, started_at, status)
CronJobID: jid, VALUES ($1, $2, $3, $4)`,
StartedAt: now, execID, jobID, now, "running",
Status: sql.NullString{String: "running", Valid: true}, )
})
dockerClient, _ := c.Get("docker_client") dockerClient, _ := c.Get("docker_client")
dockerCli, _ := dockerClient.(*docker.Client) dockerCli, _ := dockerClient.(*docker.Client)
@@ -487,25 +458,39 @@ func StartCronScheduler(ctx context.Context, db *database.DB, dockerClient *dock
} }
func runDueCronJobs(db *database.DB, dockerClient *docker.Client) { func runDueCronJobs(db *database.DB, dockerClient *docker.Client) {
q := sqlcdb.New(db.DB) rows, err := db.Query(
jobs, err := q.ListDueCronJobs(context.Background()) `SELECT id, project_id, service_id, name, schedule, command, timezone, enabled, retention
FROM cron_jobs
WHERE enabled = TRUE AND next_run_at IS NOT NULL AND next_run_at <= NOW()`)
if err != nil { if err != nil {
log.Printf("cron scheduler: failed to list due jobs: %v", err) log.Printf("cron scheduler: failed to list due jobs: %v", err)
return return
} }
defer rows.Close()
type dueJob struct {
CronJob
Command string
}
var jobs []dueJob
for rows.Next() {
var j dueJob
if err := rows.Scan(&j.ID, &j.ProjectID, &j.ServiceID, &j.Name, &j.Schedule,
&j.Command, &j.Timezone, &j.Enabled, &j.Retention); err != nil {
continue
}
jobs = append(jobs, j)
}
for _, j := range jobs { for _, j := range jobs {
execID := uuid.New().String() execID := uuid.New().String()
if err := q.InsertCronExecution(context.Background(), sqlcdb.InsertCronExecutionParams{ if _, err := db.Exec(
ID: uuid.MustParse(execID), `INSERT INTO cron_executions (id, cron_job_id, started_at, status) VALUES ($1, $2, $3, $4)`,
CronJobID: j.ID, execID, j.ID, time.Now(), "running"); err != nil {
StartedAt: time.Now(),
Status: sql.NullString{String: "running", Valid: true},
}); err != nil {
log.Printf("cron scheduler: failed to record execution for %s: %v", j.ID, err) log.Printf("cron scheduler: failed to record execution for %s: %v", j.ID, err)
continue continue
} }
go executeCronJob(db, dockerClient, j.ID.String(), j.ServiceID.String(), execID, j.Command, j.Schedule, j.Timezone.String, int(j.Retention.Int32)) go executeCronJob(db, dockerClient, j.ID, j.ServiceID, execID, j.Command, j.Schedule, j.Timezone, j.Retention)
} }
} }
@@ -599,32 +584,23 @@ func executeCronJob(db *database.DB, dockerClient *docker.Client, jobID, service
} }
now := time.Now() now := time.Now()
q := sqlcdb.New(db.DB) db.Exec(
if eid, err := uuid.Parse(execID); err == nil { `UPDATE cron_executions SET finished_at = $1, status = $2, output = $3, error = $4 WHERE id = $5`,
_ = q.FinishCronExecution(context.Background(), sqlcdb.FinishCronExecutionParams{ now, status, output, errText, execID,
FinishedAt: sql.NullTime{Time: now, Valid: true}, )
Status: sql.NullString{String: status, Valid: true},
Output: sql.NullString{String: output, Valid: true},
Error: sql.NullString{String: errText, Valid: true},
ID: eid,
})
}
next, _ := calculateNextRun(schedule, timezone) next, _ := calculateNextRun(schedule, timezone)
if jid, err := uuid.Parse(jobID); err == nil { db.Exec(
_ = q.UpdateCronJobRun(context.Background(), sqlcdb.UpdateCronJobRunParams{ `UPDATE cron_jobs SET last_run_at = $1, last_status = $2, last_output = $3, next_run_at = $4 WHERE id = $5`,
LastRunAt: sql.NullTime{Time: now, Valid: true}, now, status, output, next, jobID,
LastStatus: sql.NullString{String: status, Valid: true}, )
LastOutput: sql.NullString{String: output, Valid: true},
NextRunAt: ntPtr(next),
ID: jid,
})
if retention > 0 { if retention > 0 {
_ = q.TrimCronExecutions(context.Background(), sqlcdb.TrimCronExecutionsParams{ db.Exec(
CronJobID: jid, `DELETE FROM cron_executions WHERE cron_job_id = $1 AND id NOT IN (
Limit: int32(retention), SELECT id FROM cron_executions WHERE cron_job_id = $1
}) ORDER BY started_at DESC LIMIT $2)`,
} jobID, retention,
)
} }
} }
-127
View File
@@ -1,16 +1,9 @@
package api package api
import ( import (
"context"
"database/sql"
"strings" "strings"
"testing" "testing"
"time" "time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
) )
func TestCalculateNextRun(t *testing.T) { func TestCalculateNextRun(t *testing.T) {
@@ -71,123 +64,3 @@ func TestCalculateNextRunTimezone(t *testing.T) {
t.Logf("utc next: %v / tokyo next: %v", utcNext, tokyoNext) t.Logf("utc next: %v / tokyo next: %v", utcNext, tokyoNext)
} }
} }
// TestCronLiveInvariants exercises the cron.sql queries on live Postgres:
// insert/read/owner join, execution lifecycle, run bookkeeping, retention trim.
func TestCronLiveInvariants(t *testing.T) {
dsn := testDSNString(t)
db, err := database.NewConnection(dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer db.Close()
ctx := context.Background()
q := sqlcdb.New(db.DB)
userID := uuid.New()
projectID := uuid.New()
envID := uuid.New()
serviceID := uuid.New()
jobID := uuid.New()
suffix := strings.ReplaceAll(jobID.String()[:8], "-", "")
must := func(err error) {
t.Helper()
if err != nil {
t.Fatalf("setup: %v", err)
}
}
_, err = db.Exec(`INSERT INTO users (id, email, password_hash, name) VALUES ($1, $2, 'x', 'test')`,
userID, "cron-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "cron-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type)
VALUES ($1, $2, $3, $4, 'web', 'image')`,
serviceID, "cron-"+suffix, projectID, envID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }()
next, err := calculateNextRun("0 4 * * *", "UTC")
if err != nil {
t.Fatalf("schedule: %v", err)
}
must(q.InsertCronJob(ctx, sqlcdb.InsertCronJobParams{
ID: jobID, ProjectID: projectID, ServiceID: serviceID,
Name: "job-" + suffix, Schedule: "0 4 * * *", Command: "echo hi",
Timezone: sql.NullString{String: "UTC", Valid: true},
Enabled: sql.NullBool{Bool: true, Valid: true},
NextRunAt: ntPtr(next),
}))
job, err := q.GetCronJob(ctx, jobID)
if err != nil || job.Name != "job-"+suffix || job.Command != "echo hi" {
t.Fatalf("job: %v %+v", err, job)
}
owner, err := q.GetCronJobOwner(ctx, jobID)
if err != nil || owner != userID {
t.Fatalf("owner: %v %v", err, owner)
}
owner2, err := q.GetCronOwnerViaService(ctx, serviceID)
if err != nil || owner2 != userID {
t.Fatalf("owner via service: %v %v", err, owner2)
}
pid, err := q.GetCronJobProjectID(ctx, jobID)
if err != nil || pid != projectID {
t.Fatalf("project: %v %v", err, pid)
}
// Trigger-shaped read.
trig, err := q.GetCronJobForTrigger(ctx, jobID)
if err != nil || trig.ServiceID != serviceID || trig.OwnerID != userID {
t.Fatalf("trigger: %v %+v", err, trig)
}
// Execution lifecycle + retention trim.
for i := 0; i < 3; i++ {
execID := uuid.New()
must(q.InsertCronExecution(ctx, sqlcdb.InsertCronExecutionParams{
ID: execID, CronJobID: jobID,
StartedAt: time.Now().Add(time.Duration(i) * time.Second),
Status: sql.NullString{String: "running", Valid: true},
}))
must(q.FinishCronExecution(ctx, sqlcdb.FinishCronExecutionParams{
FinishedAt: sql.NullTime{Time: time.Now(), Valid: true},
Status: sql.NullString{String: "success", Valid: true},
Output: sql.NullString{String: "ok", Valid: true},
Error: sql.NullString{String: "", Valid: true},
ID: execID,
}))
}
execs, err := q.ListCronExecutions(ctx, jobID)
if err != nil || len(execs) != 3 {
t.Fatalf("execs: %v %d", err, len(execs))
}
if execs[0].Status.String != "success" || !execs[0].FinishedAt.Valid {
t.Fatalf("exec row: %+v", execs[0])
}
must(q.TrimCronExecutions(ctx, sqlcdb.TrimCronExecutionsParams{CronJobID: jobID, Limit: 1}))
execs, _ = q.ListCronExecutions(ctx, jobID)
if len(execs) != 1 {
t.Fatalf("trim kept %d, want 1", len(execs))
}
must(q.UpdateCronJobRun(ctx, sqlcdb.UpdateCronJobRunParams{
LastRunAt: sql.NullTime{Time: time.Now(), Valid: true},
LastStatus: sql.NullString{String: "success", Valid: true},
LastOutput: sql.NullString{String: "ok", Valid: true},
NextRunAt: ntPtr(next),
ID: jobID,
}))
job, _ = q.GetCronJob(ctx, jobID)
if job.LastStatus.String != "success" || !job.LastRunAt.Valid {
t.Fatalf("post-run job: %+v", job)
}
must(q.DeleteCronJob(ctx, jobID))
if _, err := q.GetCronJob(ctx, jobID); err == nil {
t.Fatal("deleted job still readable")
}
}
+278 -270
View File
@@ -2,14 +2,12 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
"containr/internal/docker" "containr/internal/docker"
"containr/internal/source" "containr/internal/source"
"containr/internal/types" "containr/internal/types"
"context" "context"
"database/sql"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -39,72 +37,6 @@ type DeploymentModel struct {
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
} }
// deploymentRowShape carries the deployment columns shared by the
// generated list/detail/rollback row types.
type deploymentRowShape struct {
ID uuid.UUID
ServiceID uuid.UUID
CommitHash sql.NullString
Status sql.NullString
ImageName sql.NullString
ImageTag sql.NullString
BuildLog sql.NullString
RuntimeLog sql.NullString
Error sql.NullString
StartedAt sql.NullTime
CompletedAt sql.NullTime
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}
func strPtrNS(v sql.NullString) *string {
if !v.Valid {
return nil
}
s := v.String
return &s
}
func ptrStr(s *string) string {
if s == nil {
return ""
}
return *s
}
func timePtrNT(v sql.NullTime) *time.Time {
if !v.Valid {
return nil
}
t := v.Time
return &t
}
func ntPtr(t *time.Time) sql.NullTime {
if t == nil {
return sql.NullTime{}
}
return sql.NullTime{Time: *t, Valid: true}
}
func deploymentModelFrom(r deploymentRowShape) DeploymentModel {
return DeploymentModel{
ID: r.ID,
ServiceID: r.ServiceID,
CommitHash: strPtrNS(r.CommitHash),
Status: r.Status.String,
ImageName: r.ImageName.String,
ImageTag: r.ImageTag.String,
BuildLog: r.BuildLog.String,
RuntimeLog: r.RuntimeLog.String,
Error: strPtrNS(r.Error),
StartedAt: timePtrNT(r.StartedAt),
CompletedAt: timePtrNT(r.CompletedAt),
CreatedAt: r.CreatedAt.Time,
UpdatedAt: r.UpdatedAt.Time,
}
}
type CreateDeploymentRequest struct { type CreateDeploymentRequest struct {
CommitHash string `json:"commit_hash"` CommitHash string `json:"commit_hash"`
Branch string `json:"branch"` Branch string `json:"branch"`
@@ -152,29 +84,44 @@ func handleGetDeployments(c *gin.Context) {
offset, limit := pageWindow(c, 50, 100) offset, limit := pageWindow(c, 50, 100)
q := sqlcdb.New(db.(*database.DB).DB) var total int
total, _ := q.CountDeploymentsForService(c.Request.Context(), serviceID) _ = db.(*database.DB).QueryRow(
`SELECT COUNT(*) FROM deployments WHERE service_id = $1`, serviceID,
).Scan(&total)
rows, err := q.ListDeploymentsForService(c.Request.Context(), sqlcdb.ListDeploymentsForServiceParams{ rows, err := db.(*database.DB).Query(
ServiceID: serviceID, Limit: int32(limit), Offset: int32(offset)}) `SELECT id, service_id, commit_hash, status, image_name, image_tag,
build_log, runtime_log, error, started_at, completed_at, created_at, updated_at
FROM deployments
WHERE service_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`,
serviceID, limit, offset,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"})
return return
} }
defer rows.Close()
deployments := make([]DeploymentModel, 0, len(rows)) var deployments []DeploymentModel
for _, r := range rows { for rows.Next() {
deployments = append(deployments, deploymentModelFrom(deploymentRowShape{ var d DeploymentModel
ID: r.ID, ServiceID: r.ServiceID, CommitHash: r.CommitHash, Status: r.Status, err := rows.Scan(
ImageName: r.ImageName, ImageTag: r.ImageTag, BuildLog: r.BuildLog, &d.ID, &d.ServiceID, &d.CommitHash, &d.Status, &d.ImageName, &d.ImageTag,
RuntimeLog: r.RuntimeLog, Error: r.Error, StartedAt: r.StartedAt, &d.BuildLog, &d.RuntimeLog, &d.Error, &d.StartedAt, &d.CompletedAt,
CompletedAt: r.CompletedAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, &d.CreatedAt, &d.UpdatedAt,
})) )
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan deployment"})
return
}
deployments = append(deployments, d)
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"deployments": deployments, "deployments": deployments,
"pagination": paginationMeta(offset, limit, int(total)), "pagination": paginationMeta(offset, limit, total),
}) })
} }
@@ -205,33 +152,51 @@ func handleGetRecentDeployments(c *gin.Context) {
offset, limit := pageWindow(c, 10, 50) offset, limit := pageWindow(c, 10, 50)
q := sqlcdb.New(db.(*database.DB).DB) var total int
total, err := q.CountAccessibleDeployments(c.Request.Context(), sqlcdb.CountAccessibleDeploymentsParams{ if err := db.(*database.DB).QueryRow(
OwnerID: userID, Column2: isAdmin}) `SELECT COUNT(*)
if err != nil { FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)`,
userID, isAdmin,
).Scan(&total); err != nil {
total = 0 total = 0
} }
rows, err := q.ListRecentAccessibleDeployments(c.Request.Context(), sqlcdb.ListRecentAccessibleDeploymentsParams{ rows, err := db.(*database.DB).Query(
OwnerID: userID, Column2: isAdmin, Limit: int32(limit), Offset: int32(offset)}) `SELECT d.id, d.service_id, s.name, p.name, d.status,
COALESCE(d.image_name, ''), d.started_at, d.completed_at, d.created_at
FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)
ORDER BY d.created_at DESC
LIMIT $3 OFFSET $4`,
userID, isAdmin, limit, offset,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"})
return return
} }
defer rows.Close()
deployments := make([]RecentDeployment, 0, len(rows)) deployments := []RecentDeployment{}
for _, r := range rows { for rows.Next() {
deployments = append(deployments, RecentDeployment{ var d RecentDeployment
ID: r.ID, ServiceID: r.ServiceID, ServiceName: r.ServiceName, if err := rows.Scan(&d.ID, &d.ServiceID, &d.ServiceName, &d.ProjectName,
ProjectName: r.ProjectName, Status: r.Status.String, ImageName: r.ImageName, &d.Status, &d.ImageName, &d.StartedAt, &d.CompletedAt, &d.CreatedAt); err != nil {
StartedAt: timePtrNT(r.StartedAt), CompletedAt: timePtrNT(r.CompletedAt), c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan deployment"})
CreatedAt: r.CreatedAt.Time, return
}) }
deployments = append(deployments, d)
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"deployments": deployments, "deployments": deployments,
"pagination": paginationMeta(offset, limit, int(total)), "pagination": paginationMeta(offset, limit, total),
}) })
} }
@@ -265,27 +230,38 @@ func handleCreateDeployment(c *gin.Context) {
return return
} }
ctx := c.Request.Context() var service Service
q := sqlcdb.New(db.(*database.DB).DB) var projectOwner string
sr, err := q.GetServiceForDeployWithOwner(ctx, serviceID) err = db.(*database.DB).QueryRow(
`SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at, p.owner_id
FROM services s
JOIN projects p ON s.project_id = p.id
WHERE s.id = $1`,
serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt, &projectOwner,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return return
} }
service := serviceForDeployFromRow(sqlcdb.GetServiceForDeployRow{
ID: sr.ID, ProjectID: sr.ProjectID, Name: sr.Name, Type: sr.Type,
Status: sr.Status, Image: sr.Image, Command: sr.Command,
Environment: sr.Environment, GitRepo: sr.GitRepo, GitBranch: sr.GitBranch,
BuildPath: sr.BuildPath, Cpu: sr.Cpu, Memory: sr.Memory,
Replicas: sr.Replicas, Port: sr.Port, Domain: sr.Domain,
HealthcheckPath: sr.HealthcheckPath, RestartPolicy: sr.RestartPolicy,
Builder: sr.Builder, CpuReserve: sr.CpuReserve, MemoryReserve: sr.MemoryReserve,
StaticBuildCmd: sr.StaticBuildCmd, StaticDir: sr.StaticDir,
CreatedAt: sr.CreatedAt, UpdatedAt: sr.UpdatedAt,
})
projectOwner := sr.OwnerID
if projectOwner.String() != userID.(string) { if projectOwner != userID.(string) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -311,17 +287,12 @@ func handleCreateDeployment(c *gin.Context) {
UpdatedAt: now, UpdatedAt: now,
} }
err = q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{ _, err = db.(*database.DB).Exec(
ID: d.ID, `INSERT INTO deployments
ServiceID: d.ServiceID, (id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
Version: fmt.Sprintf("v%d", now.Unix()), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
CommitHash: sql.NullString{String: ptrStr(d.CommitHash), Valid: d.CommitHash != nil}, d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt,
Status: sql.NullString{String: d.Status, Valid: true}, )
ImageName: sql.NullString{String: d.ImageName, Valid: true},
ImageTag: sql.NullString{String: d.ImageTag, Valid: true},
CreatedAt: sql.NullTime{Time: d.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: d.UpdatedAt, Valid: true},
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create deployment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create deployment"})
@@ -332,20 +303,20 @@ func handleCreateDeployment(c *gin.Context) {
if !exists || engine == nil { if !exists || engine == nil {
unavailableErr := "Deployment engine unavailable. Docker may not be configured on this server." unavailableErr := "Deployment engine unavailable. Docker may not be configured on this server."
completedAt := time.Now() completedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.(*database.DB).Exec(
Error: sql.NullString{String: unavailableErr, Valid: true}, `UPDATE deployments
CompletedAt: sql.NullTime{Time: completedAt, Valid: true}, SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
ID: d.ID, WHERE id = $3`,
}) unavailableErr, completedAt, d.ID,
)
d.Status = "failed" d.Status = "failed"
d.Error = &unavailableErr d.Error = &unavailableErr
d.CompletedAt = &completedAt d.CompletedAt = &completedAt
} else { } else {
err = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, err = db.(*database.DB).Exec(
Status: sql.NullString{String: "building", Valid: true}, `UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, time.Now(), serviceID,
ID: serviceID, )
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service status"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service status"})
return return
@@ -360,17 +331,14 @@ func handleCreateDeployment(c *gin.Context) {
}) })
if pos > 0 { if pos > 0 {
d.Status = "queued" d.Status = "queued"
now := time.Now() _, _ = db.(*database.DB).Exec(
_ = q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{ `UPDATE deployments SET status = 'queued', updated_at = $1 WHERE id = $2`,
Status: sql.NullString{String: "queued", Valid: true}, time.Now(), d.ID,
UpdatedAt: sql.NullTime{Time: now, Valid: true}, )
ID: d.ID, _, _ = db.(*database.DB).Exec(
}) `UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`,
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ time.Now(), serviceID,
Status: sql.NullString{String: "queued", Valid: true}, )
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: serviceID,
})
} }
} }
@@ -412,7 +380,6 @@ func runDeploymentAndSyncWithImage(
) { ) {
ctx, cancel := context.WithTimeout(parentCtx, 30*time.Minute) ctx, cancel := context.WithTimeout(parentCtx, 30*time.Minute)
defer cancel() defer cancel()
q := sqlcdb.New(db.DB)
// The checkout and any shipped artifacts die with this deployment. // The checkout and any shipped artifacts die with this deployment.
var artifactIDs []string var artifactIDs []string
@@ -425,11 +392,10 @@ func runDeploymentAndSyncWithImage(
}() }()
failDeploy := func(msg string) { failDeploy := func(msg string) {
failedAt := time.Now() failedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.Exec(
Error: sql.NullString{String: msg, Valid: true}, `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, msg, failedAt, dbDeployment.ID,
ID: dbDeployment.ID, )
})
} }
sourcePath := strings.TrimSpace(service.BuildPath) sourcePath := strings.TrimSpace(service.BuildPath)
@@ -457,10 +423,11 @@ func runDeploymentAndSyncWithImage(
replicas = 1 replicas = 1
} }
var publishedPort int32
// Best effort: reuse the host port from the last live deployment so public // Best effort: reuse the host port from the last live deployment so public
// URLs survive redeploys. Column exists post-migration; older DBs get // URLs survive redeploys. Column exists post-migration; older DBs get
// ephemeral ports. // ephemeral ports.
publishedPort, _ := q.GetServicePublishedPort(ctx, service.ID) _ = db.QueryRow(`SELECT published_port FROM services WHERE id = $1`, service.ID).Scan(&publishedPort)
publicPort := int32(service.Port) publicPort := int32(service.Port)
if service.Builder == "static" && publicPort == 0 { if service.Builder == "static" && publicPort == 0 {
@@ -484,9 +451,7 @@ func runDeploymentAndSyncWithImage(
// remote builds package it into a context artifact the node builds. // remote builds package it into a context artifact the node builds.
if service.GitRepo != "" && imageOverride == "" { if service.GitRepo != "" && imageOverride == "" {
var ownerID string var ownerID string
if o, err := q.GetProjectOwner(ctx, service.ProjectID); err == nil { _ = db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, service.ProjectID).Scan(&ownerID)
ownerID = o.String()
}
branch := req.Branch branch := req.Branch
if branch == "" { if branch == "" {
branch = service.GitBranch branch = service.GitBranch
@@ -570,9 +535,7 @@ func runDeploymentAndSyncWithImage(
} }
} else { } else {
var ownerID string var ownerID string
if o, err := q.GetProjectOwner(ctx, service.ProjectID); err == nil { _ = db.QueryRow(`SELECT owner_id FROM projects WHERE id = $1`, service.ProjectID).Scan(&ownerID)
ownerID = o.String()
}
auth := registryAuthFor(db, ownerID, image) auth := registryAuthFor(db, ownerID, image)
deployReq.BuildConfig = &deployment.BuildConfig{ deployReq.BuildConfig = &deployment.BuildConfig{
BuildType: "prebuilt", BuildType: "prebuilt",
@@ -642,11 +605,10 @@ func runDeploymentAndSyncWithImage(
if msg := capacityCheck(parentCtx, db, engine.DockerClient(), service, replicas); msg != "" { if msg := capacityCheck(parentCtx, db, engine.DockerClient(), service, replicas); msg != "" {
failedAt := time.Now() failedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.Exec(
Error: sql.NullString{String: msg, Valid: true}, `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, msg, failedAt, dbDeployment.ID,
ID: dbDeployment.ID, )
})
return return
} }
@@ -654,16 +616,16 @@ func runDeploymentAndSyncWithImage(
if err != nil { if err != nil {
failedAt := time.Now() failedAt := time.Now()
failure := "Failed to start deployment engine: " + err.Error() failure := "Failed to start deployment engine: " + err.Error()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.Exec(
Error: sql.NullString{String: failure, Valid: true}, `UPDATE deployments
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
ID: dbDeployment.ID, WHERE id = $3`,
}) failure, failedAt, dbDeployment.ID,
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ )
Status: sql.NullString{String: "failed", Valid: true}, _, _ = db.Exec(
UpdatedAt: sql.NullTime{Time: failedAt, Valid: true}, `UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
ID: service.ID, failedAt, service.ID,
}) )
return return
} }
@@ -678,17 +640,16 @@ func runDeploymentAndSyncWithImage(
if errors.Is(ctx.Err(), context.Canceled) { if errors.Is(ctx.Err(), context.Canceled) {
finalStatus, finalErr = "cancelled", "Deployment cancelled" finalStatus, finalErr = "cancelled", "Deployment cancelled"
} }
_ = q.CompleteDeployment(ctx, sqlcdb.CompleteDeploymentParams{ _, _ = db.Exec(
Status: sql.NullString{String: finalStatus, Valid: true}, `UPDATE deployments
Error: sql.NullString{String: finalErr, Valid: true}, SET status = $1, error = $2, completed_at = $3, updated_at = $3
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, WHERE id = $4`,
ID: dbDeployment.ID, finalStatus, finalErr, failedAt, dbDeployment.ID,
}) )
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, _ = db.Exec(
Status: sql.NullString{String: finalStatus, Valid: true}, `UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`,
UpdatedAt: sql.NullTime{Time: failedAt, Valid: true}, finalStatus, failedAt, service.ID,
ID: service.ID, )
})
return return
case <-syncTicker.C: case <-syncTicker.C:
current, getErr := engine.GetDeployment(engineDeployment.ID) current, getErr := engine.GetDeployment(engineDeployment.ID)
@@ -699,36 +660,49 @@ func runDeploymentAndSyncWithImage(
dbStatus := mapEngineStatusToDBStatus(current.Status) dbStatus := mapEngineStatusToDBStatus(current.Status)
imageName, imageTag := splitImageReference(current.ImageName, dbDeployment.ImageTag) imageName, imageTag := splitImageReference(current.ImageName, dbDeployment.ImageTag)
now := time.Now() var dbError interface{}
_ = q.SyncDeploymentProgress(ctx, sqlcdb.SyncDeploymentProgressParams{ if current.Error != "" {
Status: sql.NullString{String: dbStatus, Valid: true}, dbError = current.Error
ImageName: sql.NullString{String: imageName, Valid: true}, }
ImageTag: sql.NullString{String: imageTag, Valid: true},
BuildLog: sql.NullString{String: current.BuildLog, Valid: true}, _, _ = db.Exec(
RuntimeLog: sql.NullString{String: current.DeployLog, Valid: true}, `UPDATE deployments
Error: sql.NullString{String: current.Error, Valid: current.Error != ""}, SET status = $1,
StartedAt: ntPtr(current.StartedAt), image_name = $2,
CompletedAt: ntPtr(current.CompletedAt), image_tag = $3,
UpdatedAt: sql.NullTime{Time: now, Valid: true}, build_log = $4,
ID: dbDeployment.ID, runtime_log = $5,
}) error = $6,
started_at = $7,
completed_at = $8,
updated_at = $9
WHERE id = $10`,
dbStatus,
imageName,
imageTag,
current.BuildLog,
current.DeployLog,
dbError,
current.StartedAt,
current.CompletedAt,
time.Now(),
dbDeployment.ID,
)
switch dbStatus { switch dbStatus {
case "deployed": case "deployed":
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, _ = db.Exec(
Status: sql.NullString{String: "running", Valid: true}, `UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: now, Valid: true}, time.Now(), service.ID,
ID: service.ID, )
})
insertUserNotification(db, userID, "deployment", "Deployment succeeded", insertUserNotification(db, userID, "deployment", "Deployment succeeded",
fmt.Sprintf("Service %s is now running.", service.Name), "service", service.ID.String()) fmt.Sprintf("Service %s is now running.", service.Name), "service", service.ID.String())
return return
case "failed": case "failed":
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, _ = db.Exec(
Status: sql.NullString{String: "failed", Valid: true}, `UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: now, Valid: true}, time.Now(), service.ID,
ID: service.ID, )
})
body := fmt.Sprintf("Service %s failed to deploy.", service.Name) body := fmt.Sprintf("Service %s failed to deploy.", service.Name)
if current.Error != "" { if current.Error != "" {
body = fmt.Sprintf("Service %s failed to deploy: %s", service.Name, current.Error) body = fmt.Sprintf("Service %s failed to deploy: %s", service.Name, current.Error)
@@ -780,16 +754,21 @@ func handleCancelDeployment(c *gin.Context) {
return return
} }
ctx := c.Request.Context() var status, owner string
q := sqlcdb.New(db.(*database.DB).DB) var serviceID uuid.UUID
access, err := q.GetDeploymentAccess(ctx, deploymentID) err = db.(*database.DB).QueryRow(
`SELECT d.status, d.service_id, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1`,
deploymentID,
).Scan(&status, &serviceID, &owner)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found", "code": "NOT_FOUND"}) c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found", "code": "NOT_FOUND"})
return return
} }
serviceID := access.ServiceID if owner != userID.(string) && !contextIsAdmin(c) {
status := access.Status.String
if access.OwnerID.String() != userID.(string) && !contextIsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied", "code": "FORBIDDEN"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied", "code": "FORBIDDEN"})
return return
} }
@@ -803,17 +782,14 @@ func handleCancelDeployment(c *gin.Context) {
switch getDeployQueue(c).Cancel(serviceID, deploymentID) { switch getDeployQueue(c).Cancel(serviceID, deploymentID) {
case deployqueue.WasQueued: case deployqueue.WasQueued:
now := time.Now() now := time.Now()
_ = q.CompleteDeployment(ctx, sqlcdb.CompleteDeploymentParams{ _, _ = db.(*database.DB).Exec(
Status: sql.NullString{String: "cancelled", Valid: true}, `UPDATE deployments SET status = 'cancelled', error = 'Deployment cancelled', completed_at = $1, updated_at = $1 WHERE id = $2`,
Error: sql.NullString{String: "Deployment cancelled", Valid: true}, now, deploymentID,
CompletedAt: sql.NullTime{Time: now, Valid: true}, )
ID: deploymentID, _, _ = db.(*database.DB).Exec(
}) `UPDATE services SET status = 'cancelled', updated_at = $1 WHERE id = $2`,
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ now, serviceID,
Status: sql.NullString{String: "cancelled", Valid: true}, )
UpdatedAt: sql.NullTime{Time: now, Valid: true},
ID: serviceID,
})
c.JSON(http.StatusOK, gin.H{"status": "cancelled"}) c.JSON(http.StatusOK, gin.H{"status": "cancelled"})
case deployqueue.WasActive: case deployqueue.WasActive:
c.JSON(http.StatusAccepted, gin.H{"status": "cancelling"}) c.JSON(http.StatusAccepted, gin.H{"status": "cancelling"})
@@ -856,18 +832,27 @@ func handleGetDeployment(c *gin.Context) {
return return
} }
r, err := sqlcdb.New(db.(*database.DB).DB).GetDeploymentWithProject(c.Request.Context(), deploymentID) var d DeploymentModel
var projectID uuid.UUID
err = db.(*database.DB).QueryRow(
`SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1`,
deploymentID,
).Scan(
&d.ID, &d.ServiceID, &d.CommitHash, &d.Status, &d.ImageName, &d.ImageTag,
&d.BuildLog, &d.RuntimeLog, &d.Error, &d.StartedAt, &d.CompletedAt,
&d.CreatedAt, &d.UpdatedAt, &projectID,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
return return
} }
projectID := r.ProjectID
d := deploymentModelFrom(deploymentRowShape{
ID: r.ID, ServiceID: r.ServiceID, CommitHash: r.CommitHash, Status: r.Status,
ImageName: r.ImageName, ImageTag: r.ImageTag, BuildLog: r.BuildLog,
RuntimeLog: r.RuntimeLog, Error: r.Error, StartedAt: r.StartedAt,
CompletedAt: r.CompletedAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
})
if _, allowed := projectReadAccess(c, db.(*database.DB), projectID); !allowed { if _, allowed := projectReadAccess(c, db.(*database.DB), projectID); !allowed {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
@@ -897,23 +882,33 @@ func handleRollbackDeployment(c *gin.Context) {
return return
} }
ctx := c.Request.Context() var targetDeployment DeploymentModel
q := sqlcdb.New(db.(*database.DB).DB) var serviceID uuid.UUID
r, err := q.GetDeploymentForRollback(ctx, deploymentID) var ownerCheck string
err = db.(*database.DB).QueryRow(
`SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1`,
deploymentID,
).Scan(
&targetDeployment.ID, &serviceID, &targetDeployment.CommitHash, &targetDeployment.Status,
&targetDeployment.ImageName, &targetDeployment.ImageTag, &targetDeployment.BuildLog,
&targetDeployment.RuntimeLog, &targetDeployment.Error, &targetDeployment.StartedAt,
&targetDeployment.CompletedAt, &targetDeployment.CreatedAt, &targetDeployment.UpdatedAt,
&ownerCheck,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
return return
} }
serviceID := r.ServiceID
targetDeployment := deploymentModelFrom(deploymentRowShape{
ID: r.ID, ServiceID: r.ServiceID, CommitHash: r.CommitHash, Status: r.Status,
ImageName: r.ImageName, ImageTag: r.ImageTag, BuildLog: r.BuildLog,
RuntimeLog: r.RuntimeLog, Error: r.Error, StartedAt: r.StartedAt,
CompletedAt: r.CompletedAt, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
})
ownerCheck := r.OwnerID
if ownerCheck.String() != userID.(string) && !contextIsAdmin(c) { if ownerCheck != userID.(string) && !contextIsAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return return
} }
@@ -936,28 +931,23 @@ func handleRollbackDeployment(c *gin.Context) {
UpdatedAt: now, UpdatedAt: now,
} }
err = q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{ _, err = db.(*database.DB).Exec(
ID: rollback.ID, `INSERT INTO deployments
ServiceID: rollback.ServiceID, (id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
Version: fmt.Sprintf("rollback-%d", now.Unix()), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
CommitHash: sql.NullString{String: ptrStr(rollback.CommitHash), Valid: rollback.CommitHash != nil}, rollback.ID, rollback.ServiceID, fmt.Sprintf("rollback-%d", now.Unix()), rollback.CommitHash, rollback.Status,
Status: sql.NullString{String: rollback.Status, Valid: true}, rollback.ImageName, rollback.ImageTag, rollback.CreatedAt, rollback.UpdatedAt,
ImageName: sql.NullString{String: rollback.ImageName, Valid: true}, )
ImageTag: sql.NullString{String: rollback.ImageTag, Valid: true},
CreatedAt: sql.NullTime{Time: rollback.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: rollback.UpdatedAt, Valid: true},
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create rollback deployment"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create rollback deployment"})
return return
} }
err = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, err = db.(*database.DB).Exec(
Status: sql.NullString{String: "building", Valid: true}, `UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, time.Now(), serviceID,
ID: serviceID, )
})
// Real rollback: redeploy the target deployment's image. // Real rollback: redeploy the target deployment's image.
engineValue, _ := c.Get("deployment_engine") engineValue, _ := c.Get("deployment_engine")
@@ -973,18 +963,36 @@ func handleRollbackDeployment(c *gin.Context) {
if engine == nil { if engine == nil {
reason = "Deployment engine unavailable. Docker may not be configured on this server." reason = "Deployment engine unavailable. Docker may not be configured on this server."
} }
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ _, _ = db.(*database.DB).Exec(
Error: sql.NullString{String: reason, Valid: true}, `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
CompletedAt: sql.NullTime{Time: completedAt, Valid: true}, reason, completedAt, rollbackID,
ID: rollbackID, )
}) _, _ = db.(*database.DB).Exec(
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ `UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
Status: sql.NullString{String: "failed", Valid: true}, completedAt, serviceID,
UpdatedAt: sql.NullTime{Time: completedAt, Valid: true}, )
ID: serviceID,
})
} else { } else {
service, _ := loadServiceForDeploy(db.(*database.DB), serviceID) var service Service
_ = db.(*database.DB).QueryRow(
`SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at
FROM services s WHERE s.id = $1`, serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt,
)
rollbackReq := CreateDeploymentRequest{ rollbackReq := CreateDeploymentRequest{
CommitHash: func() string { CommitHash: func() string {
+1 -262
View File
@@ -2,11 +2,10 @@ package api
import ( import (
"context" "context"
"database/sql"
"os" "os"
"strings" "strings"
"testing"
"time" "time"
"testing"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb" "containr/internal/database/sqlcdb"
@@ -339,263 +338,3 @@ func TestPreviewEnvironmentsLiveInvariants(t *testing.T) {
} }
must(q.DeletePreviewEnvironment(ctx, previewID)) 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)
}
}
+1 -1
View File
@@ -116,7 +116,7 @@ func handleDeleteEnvironment(c *gin.Context) {
return return
} }
var projectID uuid.UUID var projectID uuid.UUID
if projectID, err = sqlcdb.New(db.DB).GetEnvironmentProjectID(context.Background(), envID); err != nil { if err := db.QueryRow(`SELECT project_id FROM environments WHERE id = $1`, envID).Scan(&projectID); err != nil {
respondError(c, http.StatusNotFound, "NOT_FOUND", "Environment not found") respondError(c, http.StatusNotFound, "NOT_FOUND", "Environment not found")
return return
} }
+52 -65
View File
@@ -17,11 +17,9 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/gateway" "containr/internal/gateway"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -109,28 +107,23 @@ func extractGatewayKey(c *gin.Context) string {
// lookupGatewayService resolves the slug to an enabled service row. // lookupGatewayService resolves the slug to an enabled service row.
func lookupGatewayService(ctx context.Context, db *database.DB, slug string) (*gatewayService, error) { func lookupGatewayService(ctx context.Context, db *database.DB, slug string) (*gatewayService, error) {
r, err := sqlcdb.New(db.DB).GetAPServiceBySlug(ctx, slug) var s gatewayService
var enabled int
err := db.QueryRowContext(ctx, `
SELECT id, upstream_url, route_prefix, upstream_auth_header,
upstream_auth_value, enabled, rpm_limit, monthly_quota,
request_timeout_ms
FROM api_services WHERE slug = $1
`, slug).Scan(&s.ID, &s.UpstreamURL, &s.RoutePrefix,
&s.UpstreamAuthHeader, &s.UpstreamAuthValue, &enabled,
&s.RPMLimit, &s.MonthlyQuota, &s.RequestTimeoutMS)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s := gatewayService{ s.Enabled = enabled != 0
ID: r.ID.String(),
UpstreamURL: r.UpstreamUrl,
RoutePrefix: r.RoutePrefix,
UpstreamAuthHeader: r.UpstreamAuthHeader,
UpstreamAuthValue: r.UpstreamAuthValue,
Enabled: r.Enabled != 0,
RPMLimit: nullInt32To64(r.RpmLimit),
MonthlyQuota: nullInt32To64(r.MonthlyQuota),
RequestTimeoutMS: int(r.RequestTimeoutMs.Int32),
}
return &s, nil return &s, nil
} }
func nullInt32To64(v sql.NullInt32) sql.NullInt64 {
return sql.NullInt64{Int64: int64(v.Int32), Valid: v.Valid}
}
// authenticateGatewayKey resolves a presented key by its indexed prefix and // authenticateGatewayKey resolves a presented key by its indexed prefix and
// bcrypt-verifies candidates. Returns nil when nothing matches. // bcrypt-verifies candidates. Returns nil when nothing matches.
func authenticateGatewayKey(ctx context.Context, db *database.DB, presented string) (*gatewayKey, error) { func authenticateGatewayKey(ctx context.Context, db *database.DB, presented string) (*gatewayKey, error) {
@@ -138,24 +131,26 @@ func authenticateGatewayKey(ctx context.Context, db *database.DB, presented stri
if len(prefix) > 8 { if len(prefix) > 8 {
prefix = prefix[:8] prefix = prefix[:8]
} }
rows, err := sqlcdb.New(db.DB).ListAPKeysByPrefix(ctx, prefix) rows, err := db.QueryContext(ctx, `
SELECT id, key_hash, enabled, rpm_limit, monthly_quota, allowed_service_ids
FROM api_keys WHERE key_prefix = $1
`, prefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, r := range rows { defer rows.Close()
k := gatewayKey{ for rows.Next() {
ID: r.ID.String(), var k gatewayKey
Hash: r.KeyHash, var enabled int
Enabled: r.Enabled != 0, if err := rows.Scan(&k.ID, &k.Hash, &enabled, &k.RPMLimit, &k.MonthlyQuota, &k.AllowedServiceIDs); err != nil {
RPMLimit: nullInt32To64(r.RpmLimit), return nil, err
MonthlyQuota: nullInt32To64(r.MonthlyQuota),
AllowedServiceIDs: r.AllowedServiceIds,
} }
k.Enabled = enabled != 0
if bcrypt.CompareHashAndPassword([]byte(k.Hash), []byte(presented)) == nil { if bcrypt.CompareHashAndPassword([]byte(k.Hash), []byte(presented)) == nil {
return &k, nil return &k, nil
} }
} }
return nil, nil return nil, rows.Err()
} }
// keyAllowsService applies the allowed_service_ids JSON allowlist. An empty // keyAllowsService applies the allowed_service_ids JSON allowlist. An empty
@@ -173,56 +168,48 @@ func keyAllowsService(k *gatewayKey, serviceID string) bool {
return false return false
} }
// monthlyKeyUsage / monthlyServiceUsage sum a counter dimension for the // monthlyUsage sums a counter dimension for the current calendar month.
// current calendar month. func monthlyUsage(ctx context.Context, db *database.DB, column, id, period string) (int, error) {
func monthlyKeyUsage(ctx context.Context, db *database.DB, id, period string) (int64, error) { var total int
keyUUID, _ := uuid.Parse(id) err := db.QueryRowContext(ctx, `
return sqlcdb.New(db.DB).SumUsageByKey(ctx, sqlcdb.SumUsageByKeyParams{ SELECT COALESCE(SUM(request_count), 0) FROM usage_counters
ApiKeyID: keyUUID, PeriodMonth: period, WHERE `+column+` = $1 AND period_month = $2
}) `, id, period).Scan(&total)
} return total, err
func monthlyServiceUsage(ctx context.Context, db *database.DB, id, period string) (int64, error) {
svcUUID, _ := uuid.Parse(id)
return sqlcdb.New(db.DB).SumUsageByService(ctx, sqlcdb.SumUsageByServiceParams{
ServiceID: svcUUID, PeriodMonth: period,
})
} }
// recordGatewayRequest persists usage, metrics, and failure incidents for a // recordGatewayRequest persists usage, metrics, and failure incidents for a
// proxied call. Runs in the request path but is three small writes. // proxied call. Runs in the request path but is three small writes.
func recordGatewayRequest(ctx context.Context, db *database.DB, svc *gatewayService, k *gatewayKey, status int, period string) { func recordGatewayRequest(ctx context.Context, db *database.DB, svc *gatewayService, k *gatewayKey, status int, period string) {
q := sqlcdb.New(db.DB) _, _ = db.ExecContext(ctx, `
keyUUID, _ := uuid.Parse(k.ID) INSERT INTO usage_counters (api_key_id, service_id, period_month, request_count, updated_at)
svcUUID, _ := uuid.Parse(svc.ID) VALUES ($1, $2, $3, 1, NOW())
_ = q.UpsertUsageCounter(ctx, sqlcdb.UpsertUsageCounterParams{ ON CONFLICT (api_key_id, service_id, period_month)
ApiKeyID: keyUUID, ServiceID: svcUUID, PeriodMonth: period, DO UPDATE SET request_count = usage_counters.request_count + 1, updated_at = NOW()
}) `, k.ID, svc.ID, period)
_ = q.TouchAPKeyLastUsed(ctx, keyUUID) _, _ = db.ExecContext(ctx, `
UPDATE api_keys SET last_used_at = NOW() WHERE id = $1
`, k.ID)
labels, _ := json.Marshal(map[string]interface{}{ labels, _ := json.Marshal(map[string]interface{}{
"service_id": svc.ID, "service_id": svc.ID,
"key_id": k.ID, "key_id": k.ID,
"status": status, "status": status,
}) })
_ = q.InsertMetricPoint(ctx, sql.NullString{String: string(labels), Valid: true}) _, _ = db.ExecContext(ctx, `
INSERT INTO metrics_timeseries (metric, value, labels_json, occurred_at)
VALUES ('gateway_request', 1, $1, NOW())
`, string(labels))
} }
func recordGatewayIncident(ctx context.Context, db *database.DB, svcID string, keyID *string, code, msg, severity string, status int) { func recordGatewayIncident(ctx context.Context, db *database.DB, svcID string, keyID *string, code, msg, severity string, status int) {
var kid uuid.NullUUID var kid interface{}
if keyID != nil { if keyID != nil {
if u, err := uuid.Parse(*keyID); err == nil { kid = *keyID
kid = uuid.NullUUID{UUID: u, Valid: true}
}
} }
svcUUID, _ := uuid.Parse(svcID) _, _ = db.ExecContext(ctx, `
_ = sqlcdb.New(db.DB).InsertGatewayIncident(ctx, sqlcdb.InsertGatewayIncidentParams{ INSERT INTO incident_events (service_id, api_key_id, code, message, severity, http_status, occurred_at)
ServiceID: uuid.NullUUID{UUID: svcUUID, Valid: svcUUID != uuid.Nil}, VALUES ($1, $2, $3, $4, $5, $6, NOW())
ApiKeyID: kid, `, svcID, kid, code, msg, severity, status)
Code: code,
Message: msg,
Severity: severity,
HttpStatus: sql.NullInt32{Int32: int32(status), Valid: true},
})
} }
// handleGatewayProxy is mounted at /g/:slug and /g/:slug/*path on the root // handleGatewayProxy is mounted at /g/:slug and /g/:slug/*path on the root
@@ -280,13 +267,13 @@ func handleGatewayProxy(c *gin.Context) {
period := now.Format("2006-01") period := now.Format("2006-01")
if key.MonthlyQuota.Valid && key.MonthlyQuota.Int64 > 0 { if key.MonthlyQuota.Valid && key.MonthlyQuota.Int64 > 0 {
if used, err := monthlyKeyUsage(ctx, db, key.ID, period); err == nil && used >= key.MonthlyQuota.Int64 { if used, err := monthlyUsage(ctx, db, "api_key_id", key.ID, period); err == nil && int64(used) >= key.MonthlyQuota.Int64 {
respondError(c, http.StatusTooManyRequests, "QUOTA_EXCEEDED", "Monthly request quota exceeded") respondError(c, http.StatusTooManyRequests, "QUOTA_EXCEEDED", "Monthly request quota exceeded")
return return
} }
} }
if svc.MonthlyQuota.Valid && svc.MonthlyQuota.Int64 > 0 { if svc.MonthlyQuota.Valid && svc.MonthlyQuota.Int64 > 0 {
if used, err := monthlyServiceUsage(ctx, db, svc.ID, period); err == nil && used >= svc.MonthlyQuota.Int64 { if used, err := monthlyUsage(ctx, db, "service_id", svc.ID, period); err == nil && int64(used) >= svc.MonthlyQuota.Int64 {
respondError(c, http.StatusTooManyRequests, "QUOTA_EXCEEDED", "Service monthly quota exceeded") respondError(c, http.StatusTooManyRequests, "QUOTA_EXCEEDED", "Service monthly quota exceeded")
return return
} }
+221 -197
View File
@@ -2,8 +2,6 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"context"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
@@ -94,73 +92,41 @@ type CreateWebhookRequest struct {
Branch string `json:"branch"` Branch string `json:"branch"`
} }
func gitRepoFrom(r sqlcdb.ListGitRepositoriesRow) GitRepository {
return GitRepository{
ID: r.ID.String(),
ProviderID: r.ProviderID.String(),
Name: r.Name,
FullName: r.FullName,
Description: r.Description,
CloneURL: r.CloneUrl,
WebhookURL: r.WebhookUrl,
DefaultBranch: r.DefaultBranch.String,
IsPrivate: r.IsPrivate.Bool,
UserID: r.UserID.String(),
CreatedAt: r.CreatedAt.Time,
UpdatedAt: r.UpdatedAt.Time,
}
}
func gitRepoFromFullNameRow(r sqlcdb.GetGitRepositoryByFullNameRow) GitRepository {
return GitRepository{
ID: r.ID.String(),
ProviderID: r.ProviderID.String(),
Name: r.Name,
FullName: r.FullName,
Description: r.Description,
CloneURL: r.CloneUrl,
WebhookURL: r.WebhookUrl,
DefaultBranch: r.DefaultBranch.String,
IsPrivate: r.IsPrivate.Bool,
UserID: r.UserID.String(),
CreatedAt: r.CreatedAt.Time,
UpdatedAt: r.UpdatedAt.Time,
}
}
func gitProviderFrom(p sqlcdb.ListGitProvidersRow) GitProvider {
return GitProvider{
ID: p.ID.String(),
Name: p.Name,
DisplayName: p.DisplayName,
APIUrl: p.ApiUrl,
WebhookUrl: p.WebhookUrl,
UserID: p.UserID.String(),
CreatedAt: p.CreatedAt.Time,
UpdatedAt: p.UpdatedAt.Time,
}
}
func handleGetGitProviders(c *gin.Context) { func handleGetGitProviders(c *gin.Context) {
ctx, ok := requireGitRequestContext(c) ctx, ok := requireGitRequestContext(c)
if !ok { if !ok {
return return
} }
userID, err := uuid.Parse(ctx.userID) rows, err := ctx.db.Query(`
if err != nil { SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) FROM git_providers
return WHERE user_id = $1
} ORDER BY created_at DESC
rows, err := sqlcdb.New(ctx.db.DB).ListGitProviders(context.Background(), userID) `, ctx.userID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
defer rows.Close()
providers := make([]GitProvider, 0, len(rows)) providers := make([]GitProvider, 0)
for _, p := range rows { for rows.Next() {
providers = append(providers, gitProviderFrom(p)) var provider GitProvider
if err := rows.Scan(
&provider.ID,
&provider.Name,
&provider.DisplayName,
&provider.APIUrl,
&provider.WebhookUrl,
&provider.UserID,
&provider.CreatedAt,
&provider.UpdatedAt,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
providers = append(providers, provider)
} }
c.JSON(http.StatusOK, gin.H{"providers": providers}) c.JSON(http.StatusOK, gin.H{"providers": providers})
@@ -207,22 +173,21 @@ func handleConnectGitHubApp(c *gin.Context) {
UserID: ctx.userID, UserID: ctx.userID,
} }
prow, err := sqlcdb.New(ctx.db.DB).UpsertGitHubAppProvider(context.Background(), sqlcdb.UpsertGitHubAppProviderParams{ if err := ctx.db.QueryRow(`
ID: uuid.MustParse(provider.ID), INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
Name: provider.Name, VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
DisplayName: provider.DisplayName, ON CONFLICT (name, user_id)
ApiUrl: provider.APIUrl, DO UPDATE SET
WebhookUrl: provider.WebhookUrl, display_name = EXCLUDED.display_name,
AccessToken: provider.AccessToken, api_url = EXCLUDED.api_url,
UserID: uuid.MustParse(provider.UserID), webhook_url = EXCLUDED.webhook_url,
}) access_token = EXCLUDED.access_token,
if err != nil { updated_at = NOW()
RETURNING id, created_at, updated_at
`, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl, provider.WebhookUrl, provider.AccessToken, provider.UserID).Scan(&provider.ID, &provider.CreatedAt, &provider.UpdatedAt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect GitHub App installation"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect GitHub App installation"})
return return
} }
provider.ID = prow.ID.String()
provider.CreatedAt = prow.CreatedAt.Time
provider.UpdatedAt = prow.UpdatedAt.Time
provider.AccessToken = "" provider.AccessToken = ""
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -294,16 +259,10 @@ func handleCreateGitProvider(c *gin.Context) {
return return
} }
q := sqlcdb.New(ctx.db.DB) if _, err := ctx.db.Exec(`
if err := q.InsertGitProvider(context.Background(), sqlcdb.InsertGitProviderParams{ INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
ID: uuid.MustParse(provider.ID), VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
Name: provider.Name, `, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl, provider.WebhookUrl, provider.AccessToken, provider.UserID); err != nil {
DisplayName: provider.DisplayName,
ApiUrl: provider.APIUrl,
WebhookUrl: provider.WebhookUrl,
AccessToken: provider.AccessToken,
UserID: uuid.MustParse(provider.UserID),
}); err != nil {
if isUniqueConstraintError(err) { if isUniqueConstraintError(err) {
c.JSON(http.StatusConflict, gin.H{"error": "Provider already connected for this account"}) c.JSON(http.StatusConflict, gin.H{"error": "Provider already connected for this account"})
return return
@@ -312,12 +271,13 @@ func handleCreateGitProvider(c *gin.Context) {
return return
} }
if ts, err := q.GetGitProviderTimestamps(context.Background(), uuid.MustParse(provider.ID)); err != nil { if err := ctx.db.QueryRow(`
SELECT created_at, updated_at
FROM git_providers
WHERE id = $1
`, provider.ID).Scan(&provider.CreatedAt, &provider.UpdatedAt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read created provider"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read created provider"})
return return
} else {
provider.CreatedAt = ts.CreatedAt.Time
provider.UpdatedAt = ts.UpdatedAt.Time
} }
provider.AccessToken = "" provider.AccessToken = ""
@@ -336,11 +296,12 @@ func handleGetGitRepositories(c *gin.Context) {
return return
} }
q := sqlcdb.New(ctx.db.DB) var providerName, providerDisplayName, providerAccessToken, providerAPIURL string
provRow, err := q.GetGitProviderForRepos(context.Background(), sqlcdb.GetGitProviderForReposParams{ err := ctx.db.QueryRow(`
ID: uuid.MustParse(providerID), SELECT name, display_name, access_token, api_url
UserID: uuid.MustParse(ctx.userID), FROM git_providers
}) WHERE id = $1 AND user_id = $2
`, providerID, ctx.userID).Scan(&providerName, &providerDisplayName, &providerAccessToken, &providerAPIURL)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
@@ -349,10 +310,6 @@ func handleGetGitRepositories(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
providerName := provRow.Name
providerDisplayName := provRow.DisplayName
providerAccessToken := provRow.AccessToken
providerAPIURL := provRow.ApiUrl
if c.DefaultQuery("sync", "1") != "0" { if c.DefaultQuery("sync", "1") != "0" {
if syncErr := syncProviderRepositories(ctx.db, providerID, ctx.userID, providerName, providerAPIURL, providerAccessToken); syncErr != nil { if syncErr := syncProviderRepositories(ctx.db, providerID, ctx.userID, providerName, providerAPIURL, providerAccessToken); syncErr != nil {
@@ -367,30 +324,53 @@ func handleGetGitRepositories(c *gin.Context) {
} }
offset := (page - 1) * limit offset := (page - 1) * limit
search := strings.TrimSpace(c.Query("search")) search := strings.TrimSpace(c.Query("search"))
searchNullable := sql.NullString{String: search, Valid: search != ""}
repoRows, err := q.ListGitRepositories(context.Background(), sqlcdb.ListGitRepositoriesParams{ rows, err := ctx.db.Query(`
ProviderID: uuid.MustParse(providerID), SELECT id, provider_id, name, full_name, COALESCE(description, ''), clone_url, COALESCE(webhook_url, ''),
UserID: uuid.MustParse(ctx.userID), default_branch, is_private, user_id, created_at, updated_at
Column3: search, FROM git_repositories
Limit: int32(limit), WHERE provider_id = $1 AND user_id = $2
Offset: int32(offset), AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'))
}) ORDER BY updated_at DESC
LIMIT $4 OFFSET $5
`, providerID, ctx.userID, searchNullable, limit, offset)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
defer rows.Close()
repositories := make([]GitRepository, 0, len(repoRows)) repositories := make([]GitRepository, 0)
for _, r := range repoRows { for rows.Next() {
repositories = append(repositories, gitRepoFrom(r)) var repo GitRepository
if err := rows.Scan(
&repo.ID,
&repo.ProviderID,
&repo.Name,
&repo.FullName,
&repo.Description,
&repo.CloneURL,
&repo.WebhookURL,
&repo.DefaultBranch,
&repo.IsPrivate,
&repo.UserID,
&repo.CreatedAt,
&repo.UpdatedAt,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
repositories = append(repositories, repo)
} }
total, err := q.CountGitRepositories(context.Background(), sqlcdb.CountGitRepositoriesParams{ var total int
ProviderID: uuid.MustParse(providerID), if err := ctx.db.QueryRow(`
UserID: uuid.MustParse(ctx.userID), SELECT COUNT(*)
Column3: search, FROM git_repositories
}) WHERE provider_id = $1 AND user_id = $2
if err != nil { AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'))
`, providerID, ctx.userID, searchNullable).Scan(&total); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
@@ -428,11 +408,12 @@ func handleConnectGitRepository(c *gin.Context) {
return return
} }
q := sqlcdb.New(ctx.db.DB) var providerName string
providerName, err := q.GetGitProviderName(context.Background(), sqlcdb.GetGitProviderNameParams{ err := ctx.db.QueryRow(`
ID: uuid.MustParse(providerID), SELECT name
UserID: uuid.MustParse(ctx.userID), FROM git_providers
}) WHERE id = $1 AND user_id = $2
`, providerID, ctx.userID).Scan(&providerName)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
@@ -448,16 +429,31 @@ func handleConnectGitRepository(c *gin.Context) {
return return
} }
existingRow, err := q.GetGitRepositoryByFullName(context.Background(), sqlcdb.GetGitRepositoryByFullNameParams{ var existing GitRepository
ProviderID: uuid.MustParse(providerID), err = ctx.db.QueryRow(`
FullName: fullName, SELECT id, provider_id, name, full_name, COALESCE(description, ''), clone_url, COALESCE(webhook_url, ''),
UserID: uuid.MustParse(ctx.userID), default_branch, is_private, user_id, created_at, updated_at
}) FROM git_repositories
WHERE provider_id = $1 AND full_name = $2 AND user_id = $3
`, providerID, fullName, ctx.userID).Scan(
&existing.ID,
&existing.ProviderID,
&existing.Name,
&existing.FullName,
&existing.Description,
&existing.CloneURL,
&existing.WebhookURL,
&existing.DefaultBranch,
&existing.IsPrivate,
&existing.UserID,
&existing.CreatedAt,
&existing.UpdatedAt,
)
switch { switch {
case err == nil: case err == nil:
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "Repository already connected", "message": "Repository already connected",
"repository": gitRepoFromFullNameRow(existingRow), "repository": existing,
}) })
return return
case !errors.Is(err, sql.ErrNoRows): case !errors.Is(err, sql.ErrNoRows):
@@ -477,18 +473,13 @@ func handleConnectGitRepository(c *gin.Context) {
UserID: ctx.userID, UserID: ctx.userID,
} }
ins, err := q.InsertGitRepository(context.Background(), sqlcdb.InsertGitRepositoryParams{ if err := ctx.db.QueryRow(`
ID: uuid.MustParse(repo.ID), INSERT INTO git_repositories (
ProviderID: uuid.MustParse(repo.ProviderID), id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at
Name: repo.Name, )
FullName: repo.FullName, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
Description: sql.NullString{String: repo.Description, Valid: true}, RETURNING created_at, updated_at
CloneUrl: repo.CloneURL, `, repo.ID, repo.ProviderID, repo.Name, repo.FullName, repo.Description, repo.CloneURL, repo.DefaultBranch, repo.IsPrivate, repo.UserID).Scan(&repo.CreatedAt, &repo.UpdatedAt); err != nil {
DefaultBranch: sql.NullString{String: repo.DefaultBranch, Valid: true},
IsPrivate: sql.NullBool{Bool: repo.IsPrivate, Valid: true},
UserID: uuid.MustParse(repo.UserID),
})
if err != nil {
if isUniqueConstraintError(err) { if isUniqueConstraintError(err) {
c.JSON(http.StatusConflict, gin.H{"error": "Repository is already connected"}) c.JSON(http.StatusConflict, gin.H{"error": "Repository is already connected"})
return return
@@ -496,8 +487,6 @@ func handleConnectGitRepository(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"})
return return
} }
repo.CreatedAt = ins.CreatedAt.Time
repo.UpdatedAt = ins.UpdatedAt.Time
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{
"message": "Repository connected successfully", "message": "Repository connected successfully",
@@ -527,10 +516,12 @@ func handleCreateWebhook(c *gin.Context) {
return return
} }
providerUUID, err := sqlcdb.New(ctx.db.DB).GetGitRepositoryProviderID(context.Background(), sqlcdb.GetGitRepositoryProviderIDParams{ var providerID string
ID: uuid.MustParse(repoID), err := ctx.db.QueryRow(`
UserID: uuid.MustParse(ctx.userID), SELECT provider_id
}) FROM git_repositories
WHERE id = $1 AND user_id = $2
`, repoID, ctx.userID).Scan(&providerID)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
@@ -539,7 +530,6 @@ func handleCreateWebhook(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
providerID := providerUUID.String()
eventsJSON, err := json.Marshal(req.Events) eventsJSON, err := json.Marshal(req.Events)
if err != nil { if err != nil {
@@ -562,22 +552,23 @@ func handleCreateWebhook(c *gin.Context) {
Active: true, Active: true,
} }
wrow, err := sqlcdb.New(ctx.db.DB).UpsertGitWebhook(context.Background(), sqlcdb.UpsertGitWebhookParams{ if err := ctx.db.QueryRow(`
ID: uuid.MustParse(webhook.ID), INSERT INTO git_webhooks (
RepoID: uuid.MustParse(webhook.RepoID), id, repo_id, provider_id, events, webhook_secret, active, branch_filter, created_at, updated_at
ProviderID: uuid.MustParse(webhook.ProviderID), )
Events: webhook.Events, VALUES ($1, $2, $3, $4, $5, TRUE, $6, NOW(), NOW())
WebhookSecret: webhook.Secret, ON CONFLICT (repo_id, provider_id)
BranchFilter: sql.NullString{String: strings.TrimSpace(req.Branch), Valid: true}, DO UPDATE SET
}) events = EXCLUDED.events,
if err != nil { webhook_secret = EXCLUDED.webhook_secret,
active = TRUE,
branch_filter = EXCLUDED.branch_filter,
updated_at = NOW()
RETURNING id, active, created_at, updated_at
`, webhook.ID, webhook.RepoID, webhook.ProviderID, webhook.Events, webhook.Secret, strings.TrimSpace(req.Branch)).Scan(&webhook.ID, &webhook.Active, &webhook.CreatedAt, &webhook.UpdatedAt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure webhook"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure webhook"})
return return
} }
webhook.ID = wrow.ID.String()
webhook.Active = wrow.Active.Bool
webhook.CreatedAt = wrow.CreatedAt.Time
webhook.UpdatedAt = wrow.UpdatedAt.Time
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{
"message": "Webhook configured successfully", "message": "Webhook configured successfully",
@@ -607,39 +598,69 @@ func handleGetConnectedRepositories(c *gin.Context) {
} }
offset := (page - 1) * limit offset := (page - 1) * limit
q := sqlcdb.New(ctx.db.DB) rows, err := ctx.db.Query(`
repoRows, err := q.ListConnectedRepositories(context.Background(), sqlcdb.ListConnectedRepositoriesParams{ SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.description, ''), r.clone_url,
UserID: uuid.MustParse(ctx.userID), r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at,
Limit: int32(limit), p.name as provider_name, p.display_name
Offset: int32(offset), FROM git_repositories r
}) JOIN git_providers p ON r.provider_id = p.id
WHERE r.user_id = $1
ORDER BY r.updated_at DESC
LIMIT $2 OFFSET $3
`, ctx.userID, limit, offset)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
defer rows.Close()
repositories := make([]map[string]interface{}, 0)
for rows.Next() {
var repo GitRepository
var providerName, providerDisplayName string
if err := rows.Scan(
&repo.ID,
&repo.ProviderID,
&repo.Name,
&repo.FullName,
&repo.Description,
&repo.CloneURL,
&repo.DefaultBranch,
&repo.IsPrivate,
&repo.UserID,
&repo.CreatedAt,
&repo.UpdatedAt,
&providerName,
&providerDisplayName,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
repositories := make([]map[string]interface{}, 0, len(repoRows))
for _, r := range repoRows {
repositories = append(repositories, map[string]interface{}{ repositories = append(repositories, map[string]interface{}{
"id": r.ID.String(), "id": repo.ID,
"provider_id": r.ProviderID.String(), "provider_id": repo.ProviderID,
"name": r.Name, "name": repo.Name,
"full_name": r.FullName, "full_name": repo.FullName,
"description": r.Description, "description": repo.Description,
"clone_url": r.CloneUrl, "clone_url": repo.CloneURL,
"default_branch": r.DefaultBranch.String, "default_branch": repo.DefaultBranch,
"is_private": r.IsPrivate.Bool, "is_private": repo.IsPrivate,
"created_at": r.CreatedAt.Time, "created_at": repo.CreatedAt,
"updated_at": r.UpdatedAt.Time, "updated_at": repo.UpdatedAt,
"provider": map[string]string{ "provider": map[string]string{
"name": r.ProviderName, "name": providerName,
"display_name": r.DisplayName, "display_name": providerDisplayName,
}, },
}) })
} }
total, err := q.CountUserRepositories(context.Background(), uuid.MustParse(ctx.userID)) var total int
if err != nil { if err := ctx.db.QueryRow(`
SELECT COUNT(*)
FROM git_repositories
WHERE user_id = $1
`, ctx.userID).Scan(&total); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
@@ -684,17 +705,20 @@ func syncProviderRepositories(db *database.DB, providerID, userID, providerName,
defaultBranch = "main" defaultBranch = "main"
} }
err := sqlcdb.New(db.DB).UpsertGitRepository(context.Background(), sqlcdb.UpsertGitRepositoryParams{ _, err := db.Exec(`
ID: uuid.New(), INSERT INTO git_repositories (
ProviderID: uuid.MustParse(providerID), id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at
Name: repository.Name, )
FullName: repository.FullName, VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
Description: sql.NullString{String: repository.Description, Valid: true}, ON CONFLICT (provider_id, full_name)
CloneUrl: cloneURL, DO UPDATE SET
DefaultBranch: sql.NullString{String: defaultBranch, Valid: true}, name = EXCLUDED.name,
IsPrivate: sql.NullBool{Bool: repository.IsPrivate, Valid: true}, description = EXCLUDED.description,
UserID: uuid.MustParse(userID), clone_url = EXCLUDED.clone_url,
}) default_branch = EXCLUDED.default_branch,
is_private = EXCLUDED.is_private,
updated_at = NOW()
`, uuid.NewString(), providerID, repository.Name, repository.FullName, repository.Description, cloneURL, defaultBranch, repository.IsPrivate, userID)
if err != nil { if err != nil {
return err return err
} }
@@ -1299,15 +1323,16 @@ func handleDeleteGitProvider(c *gin.Context) {
return return
} }
affected, err := sqlcdb.New(ctx.db.DB).DeleteGitProvider(context.Background(), sqlcdb.DeleteGitProviderParams{ result, err := ctx.db.Exec(
ID: uuid.MustParse(providerID), "DELETE FROM git_providers WHERE id = $1 AND user_id = $2",
UserID: uuid.MustParse(ctx.userID), providerID, ctx.userID,
}) )
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"})
return return
} }
affected, _ := result.RowsAffected()
if affected == 0 { if affected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
return return
@@ -1337,10 +1362,12 @@ func handleGetGitRepositoryBranches(c *gin.Context) {
return return
} }
pf, err := sqlcdb.New(ctx.db.DB).GetGitProviderForFetch(context.Background(), sqlcdb.GetGitProviderForFetchParams{ var providerName, providerAPIURL, providerAccessToken string
ID: uuid.MustParse(providerID), err := ctx.db.QueryRow(`
UserID: uuid.MustParse(ctx.userID), SELECT name, api_url, access_token
}) FROM git_providers
WHERE id = $1 AND user_id = $2
`, providerID, ctx.userID).Scan(&providerName, &providerAPIURL, &providerAccessToken)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
@@ -1349,9 +1376,6 @@ func handleGetGitRepositoryBranches(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
providerName := pf.Name
providerAPIURL := pf.ApiUrl
providerAccessToken := pf.AccessToken
branches, err := fetchRepositoryBranches(providerName, providerAPIURL, providerAccessToken, owner, repo) branches, err := fetchRepositoryBranches(providerName, providerAPIURL, providerAccessToken, owner, repo)
if err != nil { if err != nil {
+64 -69
View File
@@ -5,7 +5,6 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"crypto/subtle" "crypto/subtle"
"database/sql"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -15,7 +14,6 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
@@ -40,15 +38,22 @@ func handleGitWebhookPush(c *gin.Context) {
return return
} }
q := sqlcdb.New(db.DB) var (
hook, err := q.GetGitWebhookForPush(context.Background(), uuid.MustParse(webhookID)) secret string
repoID string
providerID string
branchFilter string
active bool
)
err := db.QueryRow(`
SELECT webhook_secret, repo_id, provider_id, COALESCE(branch_filter, ''), active
FROM git_webhooks WHERE id = $1`, webhookID).
Scan(&secret, &repoID, &providerID, &branchFilter, &active)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"})
return return
} }
secret := hook.WebhookSecret if !active {
branchFilter := hook.BranchFilter
if !hook.Active.Bool {
c.JSON(http.StatusForbidden, gin.H{"error": "Webhook is disabled"}) c.JSON(http.StatusForbidden, gin.H{"error": "Webhook is disabled"})
return return
} }
@@ -59,7 +64,8 @@ func handleGitWebhookPush(c *gin.Context) {
return return
} }
providerName, _ := q.GetGitProviderNameByID(context.Background(), hook.ProviderID) var providerName string
_ = db.QueryRow(`SELECT name FROM git_providers WHERE id = $1`, providerID).Scan(&providerName)
if !verifyGitWebhookSignature(providerName, secret, c.Request, body) { if !verifyGitWebhookSignature(providerName, secret, c.Request, body) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"})
@@ -112,12 +118,14 @@ func handleGitWebhookPush(c *gin.Context) {
commit = payload.After commit = payload.After
} }
repo, err := q.GetGitRepoForPush(context.Background(), hook.RepoID) var cloneURL, fullName, repoUserID string
err = db.QueryRow(
`SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1`, repoID,
).Scan(&cloneURL, &fullName, &repoUserID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
return return
} }
cloneURL, fullName, repoUserID := repo.CloneUrl, repo.FullName, repo.UserID.String()
engineValue, _ := c.Get("deployment_engine") engineValue, _ := c.Get("deployment_engine")
engine, _ := engineValue.(*deployment.DeploymentEngine) engine, _ := engineValue.(*deployment.DeploymentEngine)
@@ -139,37 +147,34 @@ func handleGitWebhookPush(c *gin.Context) {
// the pushed repo+branch. Shared by the per-repo webhook receiver and the // the pushed repo+branch. Shared by the per-repo webhook receiver and the
// GitHub App webhook endpoint. // GitHub App webhook endpoint.
func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.DeploymentEngine, cloneURL, fullName, branch, commit, repoUserID string) (int, error) { func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.DeploymentEngine, cloneURL, fullName, branch, commit, repoUserID string) (int, error) {
q := sqlcdb.New(db.DB) // Sync-style events may carry no branch — match services on repo only.
ctx := context.Background() branchClause := "s.git_branch = $1"
// Sync-style events may carry no branch — empty branch matches all. if branch == "" {
rows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{ branchClause = "TRUE"
GitBranch: sql.NullString{String: branch, Valid: true}, }
GitRepo: sql.NullString{String: cloneURL, Valid: true}, rows, err := db.Query(fmt.Sprintf(`
GitRepo_2: sql.NullString{String: fullName, Valid: true}, SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
}) s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
s.created_at, s.updated_at
FROM services s
WHERE %s AND (s.git_repo = $2 OR s.git_repo = $3)`, branchClause),
branch, cloneURL, fullName)
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer rows.Close()
var services []Service var services []Service
for _, row := range rows { for rows.Next() {
services = append(services, Service{ var s Service
ID: row.ID, if err := rows.Scan(
ProjectID: row.ProjectID, &s.ID, &s.ProjectID, &s.Name, &s.Type, &s.Status, &s.Image, &s.Command,
Name: row.Name, &s.Environment, &s.GitRepo, &s.GitBranch, &s.BuildPath, &s.CPU, &s.Memory,
Type: row.Type, &s.CreatedAt, &s.UpdatedAt,
Status: row.Status, ); err != nil {
Image: row.Image, return 0, err
Command: row.Command, }
Environment: row.Environment, services = append(services, s)
GitRepo: row.GitRepo,
GitBranch: row.GitBranch,
BuildPath: row.BuildPath,
CPU: row.Cpu,
Memory: row.Memory,
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
})
} }
enqueued := 0 enqueued := 0
@@ -187,34 +192,27 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
if err := q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{ if _, err := db.Exec(
ID: d.ID, `INSERT INTO deployments
ServiceID: d.ServiceID, (id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
Version: fmt.Sprintf("v%d", now.Unix()), VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
CommitHash: sql.NullString{String: ptrStr(d.CommitHash), Valid: d.CommitHash != nil}, d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt,
Status: sql.NullString{String: d.Status, Valid: true}, ); err != nil {
ImageName: sql.NullString{String: d.ImageName, Valid: true},
ImageTag: sql.NullString{String: d.ImageTag, Valid: true},
CreatedAt: sql.NullTime{Time: d.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: d.UpdatedAt, Valid: true},
}); err != nil {
continue continue
} }
if engine == nil { if engine == nil {
failure := "Deployment engine unavailable. Docker may not be configured on this server."
failedAt := time.Now() failedAt := time.Now()
_ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ failure := "Deployment engine unavailable. Docker may not be configured on this server."
Error: sql.NullString{String: failure, Valid: true}, _, _ = db.Exec(
CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`,
ID: d.ID, failure, failedAt, d.ID,
}) )
continue continue
} }
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ _, _ = db.Exec(
Status: sql.NullString{String: "building", Valid: true}, `UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, time.Now(), service.ID,
ID: service.ID, )
})
// Webhook pushes default to a clean build — stale layers are the // Webhook pushes default to a clean build — stale layers are the
// classic silent-rollback failure (dflow convention). // classic silent-rollback failure (dflow convention).
branchForBuild := branch branchForBuild := branch
@@ -228,17 +226,14 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.
runDeploymentAndSync(jctx, db, engine, &d, service, webhookReq, repoUserID) runDeploymentAndSync(jctx, db, engine, &d, service, webhookReq, repoUserID)
}, },
}); pos > 0 { }); pos > 0 {
now := sql.NullTime{Time: time.Now(), Valid: true} _, _ = db.Exec(
_ = q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{ `UPDATE deployments SET status = 'queued', updated_at = $1 WHERE id = $2`,
Status: sql.NullString{String: "queued", Valid: true}, time.Now(), d.ID,
UpdatedAt: now, )
ID: d.ID, _, _ = db.Exec(
}) `UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`,
_ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ time.Now(), service.ID,
Status: sql.NullString{String: "queued", Valid: true}, )
UpdatedAt: now,
ID: service.ID,
})
} }
enqueued++ enqueued++
} }
+1 -203
View File
@@ -1,16 +1,6 @@
package api package api
import ( import "testing"
"context"
"database/sql"
"strings"
"testing"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
)
func TestNormalizeRepositoryFullName(t *testing.T) { func TestNormalizeRepositoryFullName(t *testing.T) {
cases := []struct { cases := []struct {
@@ -93,195 +83,3 @@ func TestDeriveCloneURL(t *testing.T) {
}) })
} }
} }
// Live invariant test against real Postgres — set METRICS_TEST_DSN (or
// DATABASE_URL). Covers the generated git provider/repo/webhook queries plus
// the push-dispatch service matcher (empty branch must match all branches).
func TestGitLiveInvariants(t *testing.T) {
dsn := testDSNString(t)
db, err := database.NewConnection(dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer db.Close()
ctx := context.Background()
q := sqlcdb.New(db.DB)
userID := uuid.New()
projectID := uuid.New()
envID := uuid.New()
providerID := uuid.New()
repoID := uuid.New()
webhookID := uuid.New()
svcMain := uuid.New()
svcDev := uuid.New()
suffix := strings.ReplaceAll(userID.String()[:8], "-", "")
must := func(err error) {
t.Helper()
if err != nil {
t.Fatalf("setup: %v", err)
}
}
_, err = db.Exec(`INSERT INTO users (id, email, password_hash, name) VALUES ($1, $2, 'x', 'test')`,
userID, "git-"+suffix+"@test.local")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`,
projectID, "git-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }()
// Provider lifecycle.
must(q.InsertGitProvider(ctx, sqlcdb.InsertGitProviderParams{
ID: providerID, Name: "github", DisplayName: "GitHub",
ApiUrl: "https://api.github.com", WebhookUrl: "https://hooks.test",
AccessToken: "tok", UserID: userID,
}))
providers, err := q.ListGitProviders(ctx, userID)
if err != nil || len(providers) == 0 {
t.Fatalf("providers: %v %v", err, providers)
}
name, err := q.GetGitProviderNameByID(ctx, providerID)
if err != nil || name != "github" {
t.Fatalf("provider name: %v %q", err, name)
}
fetch, err := q.GetGitProviderForFetch(ctx, sqlcdb.GetGitProviderForFetchParams{ID: providerID, UserID: userID})
if err != nil || fetch.AccessToken != "tok" || fetch.ApiUrl != "https://api.github.com" {
t.Fatalf("fetch: %v %+v", err, fetch)
}
// Repository insert + search.
cloneURL := "https://github.com/acme/svc-" + suffix + ".git"
fullName := "acme/svc-" + suffix
_, err = q.InsertGitRepository(ctx, sqlcdb.InsertGitRepositoryParams{
ID: repoID, ProviderID: providerID, Name: "svc-" + suffix, FullName: fullName,
Description: sql.NullString{String: "", Valid: true},
CloneUrl: cloneURL,
DefaultBranch: sql.NullString{String: "main", Valid: true},
IsPrivate: sql.NullBool{Bool: false, Valid: true},
UserID: userID,
})
must(err)
byName, err := q.GetGitRepositoryByFullName(ctx, sqlcdb.GetGitRepositoryByFullNameParams{
ProviderID: providerID, FullName: fullName, UserID: userID,
})
if err != nil || byName.ID != repoID || byName.CloneUrl != cloneURL {
t.Fatalf("repo by full name: %v %+v", err, byName)
}
all, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{
ProviderID: providerID, UserID: userID, Column3: "", Limit: 50, Offset: 0,
})
if err != nil || len(all) == 0 {
t.Fatalf("repo list (empty search): %v %v", err, all)
}
hit, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{
ProviderID: providerID, UserID: userID, Column3: "svc-" + suffix, Limit: 50, Offset: 0,
})
if err != nil || len(hit) != 1 || hit[0].ID != repoID {
t.Fatalf("repo list (matching search): %v %v", err, hit)
}
miss, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{
ProviderID: providerID, UserID: userID, Column3: "zzz-no-match", Limit: 50, Offset: 0,
})
if err != nil || len(miss) != 0 {
t.Fatalf("repo list (non-matching search): %v %v", err, miss)
}
count, err := q.CountGitRepositories(ctx, sqlcdb.CountGitRepositoriesParams{
ProviderID: providerID, UserID: userID, Column3: "",
})
if err != nil || count < 1 {
t.Fatalf("repo count: %v %d", err, count)
}
// Upsert on (provider_id, full_name) conflict — updates rather than errors.
must(q.UpsertGitRepository(ctx, sqlcdb.UpsertGitRepositoryParams{
ID: uuid.New(), ProviderID: providerID, Name: "renamed-" + suffix, FullName: fullName,
Description: sql.NullString{String: "d", Valid: true},
CloneUrl: cloneURL,
DefaultBranch: sql.NullString{String: "main", Valid: true},
IsPrivate: sql.NullBool{Bool: true, Valid: true},
UserID: userID,
}))
byName, err = q.GetGitRepositoryByFullName(ctx, sqlcdb.GetGitRepositoryByFullNameParams{
ProviderID: providerID, FullName: fullName, UserID: userID,
})
if err != nil || byName.Name != "renamed-"+suffix {
t.Fatalf("repo after upsert: %v %+v", err, byName)
}
// Connected-repos join + webhook lifecycle.
connected, err := q.ListConnectedRepositories(ctx, sqlcdb.ListConnectedRepositoriesParams{
UserID: userID, Limit: 50, Offset: 0,
})
if err != nil || len(connected) == 0 {
t.Fatalf("connected repos: %v %v", err, connected)
}
wh, err := q.UpsertGitWebhook(ctx, sqlcdb.UpsertGitWebhookParams{
ID: webhookID, RepoID: repoID, ProviderID: providerID,
Events: "push", WebhookSecret: "sec-" + suffix,
BranchFilter: sql.NullString{String: "", Valid: true},
})
must(err)
if !wh.Active.Bool {
t.Fatalf("webhook inactive after upsert: %+v", wh)
}
push, err := q.GetGitWebhookForPush(ctx, webhookID)
if err != nil || push.WebhookSecret != "sec-"+suffix || push.ProviderID != providerID {
t.Fatalf("webhook for push: %v %+v", err, push)
}
pushRepo, err := q.GetGitRepoForPush(ctx, repoID)
if err != nil || pushRepo.CloneUrl != cloneURL || pushRepo.FullName != fullName || pushRepo.UserID != userID {
t.Fatalf("repo for push: %v %+v", err, pushRepo)
}
// Push-dispatch service matcher: branch filter and empty-branch match-all.
for id, branch := range map[uuid.UUID]string{svcMain: "main", svcDev: "dev"} {
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type, git_repo, git_branch)
VALUES ($1, $2, $3, $4, 'web', 'git', $5, $6)`,
id, "svc-"+suffix+"-"+branch, projectID, envID, cloneURL, branch)
must(err)
}
servicesFor := func(branch string) []sqlcdb.ListServicesForPushRow {
rows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{
GitBranch: sql.NullString{String: branch, Valid: true},
GitRepo: sql.NullString{String: cloneURL, Valid: true},
GitRepo_2: sql.NullString{String: fullName, Valid: true},
})
if err != nil {
t.Fatalf("services for push %q: %v", branch, err)
}
return rows
}
mainRows := servicesFor("main")
if len(mainRows) != 1 || mainRows[0].ID != svcMain || mainRows[0].GitBranch != "main" {
t.Fatalf("main branch dispatch: %+v", mainRows)
}
devRows := servicesFor("dev")
if len(devRows) != 1 || devRows[0].ID != svcDev {
t.Fatalf("dev branch dispatch: %+v", devRows)
}
allRows := servicesFor("")
if len(allRows) != 2 {
t.Fatalf("empty-branch dispatch should match all: %+v", allRows)
}
// Repo may also be referenced by its full_name.
fullRows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{
GitBranch: sql.NullString{String: "main", Valid: true},
GitRepo: sql.NullString{String: "[email protected]:other/none.git", Valid: true},
GitRepo_2: sql.NullString{String: cloneURL, Valid: true},
})
if err != nil || len(fullRows) != 1 {
t.Fatalf("full-name repo match: %v %+v", err, fullRows)
}
// Provider delete is owner-scoped.
other, err := q.DeleteGitProvider(ctx, sqlcdb.DeleteGitProviderParams{ID: providerID, UserID: uuid.New()})
if err != nil || other != 0 {
t.Fatalf("foreign delete affected %d rows: %v", other, err)
}
deleted, err := q.DeleteGitProvider(ctx, sqlcdb.DeleteGitProviderParams{ID: providerID, UserID: userID})
if err != nil || deleted != 1 {
t.Fatalf("owner delete affected %d rows: %v", deleted, err)
}
}
+18 -27
View File
@@ -1,16 +1,12 @@
package api package api
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/source" "containr/internal/source"
"github.com/google/uuid"
) )
// resolveGitCloneSpec turns a service's git_repo (clone URL, ssh remote, or // resolveGitCloneSpec turns a service's git_repo (clone URL, ssh remote, or
@@ -26,31 +22,26 @@ func resolveGitCloneSpec(db *database.DB, ownerID, gitRepo, branch, commit, buil
} }
spec := source.Spec{Branch: branch, Commit: commit, BuildPath: buildPath} spec := source.Spec{Branch: branch, Commit: commit, BuildPath: buildPath}
q := sqlcdb.New(db.DB)
ctx := context.Background()
ownerUUID, _ := uuid.Parse(ownerID) var providerID string
var providerID uuid.UUID
if !strings.Contains(cloneURL, "://") { if !strings.Contains(cloneURL, "://") {
// Bare owner/repo — the connected repository row knows the host. // Bare owner/repo — the connected repository row knows the host.
row, err := q.GetGitRepoCloneByFullName(ctx, sqlcdb.GetGitRepoCloneByFullNameParams{ err = db.QueryRow(
FullName: cloneURL, `SELECT clone_url, provider_id FROM git_repositories
UserID: ownerUUID, WHERE full_name = $1 AND user_id = $2 LIMIT 1`,
}) cloneURL, ownerID,
).Scan(&cloneURL, &providerID)
if err != nil { if err != nil {
return source.Spec{}, fmt.Errorf("repository %s is not connected — connect it under Git settings or store a clone URL", raw) return source.Spec{}, fmt.Errorf("repository %s is not connected — connect it under Git settings or store a clone URL", raw)
} }
cloneURL = row.CloneUrl
providerID = row.ProviderID
} else { } else {
// URL form — match a connected repo for its provider when possible; // URL form — match a connected repo for its provider when possible;
// an unmatched public URL still clones, just without credentials. // an unmatched public URL still clones, just without credentials.
if pid, err := q.GetGitRepoProviderByCloneURL(ctx, sqlcdb.GetGitRepoProviderByCloneURLParams{ _ = db.QueryRow(
CloneUrl: cloneURL, `SELECT provider_id FROM git_repositories
UserID: ownerUUID, WHERE clone_url = $1 AND user_id = $2 LIMIT 1`,
}); err == nil { cloneURL, ownerID,
providerID = pid ).Scan(&providerID)
}
} }
if err := source.ValidateRemoteURL(cloneURL); err != nil { if err := source.ValidateRemoteURL(cloneURL); err != nil {
@@ -58,17 +49,17 @@ func resolveGitCloneSpec(db *database.DB, ownerID, gitRepo, branch, commit, buil
} }
spec.CloneURL = cloneURL spec.CloneURL = cloneURL
if providerID == uuid.Nil { if providerID == "" {
return spec, nil return spec, nil
} }
prov, err := q.GetGitProviderForFetch(ctx, sqlcdb.GetGitProviderForFetchParams{ var name, apiURL, token string
ID: providerID, if err := db.QueryRow(
UserID: ownerUUID, `SELECT name, api_url, access_token FROM git_providers
}) WHERE id = $1 AND user_id = $2`,
if err != nil { providerID, ownerID,
).Scan(&name, &apiURL, &token); err != nil {
return spec, nil // provider row gone — anonymous clone still works for public repos return spec, nil // provider row gone — anonymous clone still works for public repos
} }
name, apiURL, token := prov.Name, prov.ApiUrl, prov.AccessToken
switch strings.ToLower(name) { switch strings.ToLower(name) {
case "github_app": case "github_app":
+4 -4
View File
@@ -15,7 +15,6 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -89,8 +88,8 @@ func handleAdminGitHubAppManifest(c *gin.Context) {
callbackURL := base + "/git/github-app/callback" callbackURL := base + "/git/github-app/callback"
manifest, err := json.Marshal(map[string]interface{}{ manifest, err := json.Marshal(map[string]interface{}{
"name": name, "name": name,
"url": base, "url": base,
"hook_attributes": map[string]interface{}{ "hook_attributes": map[string]interface{}{
"url": base + "/api/git/github-app/webhook", "url": base + "/api/git/github-app/webhook",
"active": true, "active": true,
@@ -207,8 +206,9 @@ func handleAdminGitHubAppConvert(c *gin.Context) {
// GET /admin/git/github-app — provisioning status for the settings UI. // GET /admin/git/github-app — provisioning status for the settings UI.
func handleAdminGetGitHubApp(c *gin.Context) { func handleAdminGetGitHubApp(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
var stored string
source := "none" source := "none"
if v, err := sqlcdb.New(db.DB).GetAppSetting(c.Request.Context(), settingGitHubAppID); err == nil && strings.TrimSpace(v) != "" { if err := db.QueryRow(`SELECT value FROM app_settings WHERE key = $1`, settingGitHubAppID).Scan(&stored); err == nil && strings.TrimSpace(stored) != "" {
source = "settings" source = "settings"
} else if strings.TrimSpace(os.Getenv("GITHUB_APP_ID")) != "" { } else if strings.TrimSpace(os.Getenv("GITHUB_APP_ID")) != "" {
source = "env" source = "env"
+9 -15
View File
@@ -193,8 +193,8 @@ func handleAcceptInvite(c *gin.Context) {
return return
} }
existing, err := q.CountUsersByEmail(c.Request.Context(), req.Email) var existing int
if err != nil { if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE email = $1`, req.Email).Scan(&existing); err != nil {
respondError(c, http.StatusInternalServerError, "INTERNAL", "database error") respondError(c, http.StatusInternalServerError, "INTERNAL", "database error")
return return
} }
@@ -214,23 +214,17 @@ func handleAcceptInvite(c *gin.Context) {
return return
} }
row, err := q.InsertUser(c.Request.Context(), sqlcdb.InsertUserParams{ var user User
Email: req.Email, err = db.QueryRow(`
PasswordHash: string(hashedPassword), INSERT INTO users (email, password_hash, name)
Name: strings.TrimSpace(req.Name), VALUES ($1, $2, $3)
}) RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
`, req.Email, string(hashedPassword), strings.TrimSpace(req.Name)).
Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
if err != nil { if err != nil {
respondError(c, http.StatusConflict, "CONFLICT", "user already exists") respondError(c, http.StatusConflict, "CONFLICT", "user already exists")
return return
} }
user := User{
ID: row.ID.String(),
Email: row.Email,
Name: row.Name,
AvatarURL: row.AvatarUrl,
IsAdmin: row.IsAdmin,
CreatedAt: row.CreatedAt.Time.String(),
}
userUUID, _ := uuid.Parse(user.ID) userUUID, _ := uuid.Parse(user.ID)
if err := q.MarkUserInviteUsed(c.Request.Context(), sqlcdb.MarkUserInviteUsedParams{ if err := q.MarkUserInviteUsed(c.Request.Context(), sqlcdb.MarkUserInviteUsedParams{
+7 -3
View File
@@ -9,7 +9,6 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/docker" "containr/internal/docker"
containertypes "github.com/docker/docker/api/types/container" containertypes "github.com/docker/docker/api/types/container"
@@ -61,12 +60,17 @@ func handleGetServiceMetrics(c *gin.Context) {
return return
} }
svc, err := sqlcdb.New(db.DB).GetServiceProjectAndStatus(context.Background(), serviceID) var projectID uuid.UUID
var status string
err = db.QueryRow(
`SELECT s.project_id, COALESCE(s.status, '')
FROM services s WHERE s.id = $1`,
serviceID,
).Scan(&projectID, &status)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return return
} }
projectID := svc.ProjectID
if _, allowed := projectReadAccess(c, db, projectID); !allowed { if _, allowed := projectReadAccess(c, db, projectID); !allowed {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return return
+4 -3
View File
@@ -315,9 +315,10 @@ func (r *agentNodeRunner) replicaPayload(spec deployment.RuntimeSpec, agentID st
// pullCredentials resolves the project owner's registry auth for the image // pullCredentials resolves the project owner's registry auth for the image
// host so private images pull on remote nodes. Empty map → anonymous pull. // host so private images pull on remote nodes. Empty map → anonymous pull.
func (r *agentNodeRunner) pullCredentials(projectID, imageRef string) map[string]interface{} { func (r *agentNodeRunner) pullCredentials(projectID, imageRef string) map[string]interface{} {
pid, _ := uuid.Parse(projectID) var ownerID string
ownerID, err := sqlcdb.New(r.db.DB).GetProjectOwnerText(context.Background(), pid) if err := r.db.QueryRow(
if err != nil { `SELECT owner_id FROM projects WHERE id = $1`, projectID,
).Scan(&ownerID); err != nil {
return map[string]interface{}{} return map[string]interface{}{}
} }
auth := registryAuthFor(r.db, ownerID, imageRef) auth := registryAuthFor(r.db, ownerID, imageRef)
@@ -9,7 +9,6 @@ import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb" "containr/internal/database/sqlcdb"
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@@ -30,13 +29,15 @@ func notifyAdmins(db *database.DB, kind, title, body, resourceType, resourceID s
if db == nil { if db == nil {
return return
} }
adminIDs, err := sqlcdb.New(db.DB).ListAdminUserIDs(context.Background()) rows, err := db.Query(`SELECT id FROM users WHERE is_admin`)
if err != nil { if err != nil {
log.Printf("containr: notify admins lookup failed: %v", err) log.Printf("containr: notify admins lookup failed: %v", err)
return return
} }
for _, id := range adminIDs { defer rows.Close()
{ for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err == nil {
insertUserNotification(db, id.String(), kind, title, body, resourceType, resourceID) insertUserNotification(db, id.String(), kind, title, body, resourceType, resourceID)
} }
} }
@@ -72,32 +73,34 @@ type agentHealthRow struct {
func reconcileAgentHealth(db *database.DB, staleAfter time.Duration) { func reconcileAgentHealth(db *database.DB, staleAfter time.Duration) {
cutoff := time.Now().Add(-staleAfter) cutoff := time.Now().Add(-staleAfter)
var agents []agentHealthRow var agents []agentHealthRow
q := sqlcdb.New(db.DB) rows, err := db.Query(
agentRows, err := q.ListAgentHealthRows(context.Background(), sql.NullTime{Time: cutoff, Valid: true}) `SELECT id, COALESCE(name, hostname, id::text), COALESCE(status, ''),
COALESCE(last_heartbeat < $1, TRUE), auto_prune
FROM node_agents`, cutoff)
if err != nil { if err != nil {
log.Printf("containr: agent health sweep: %v", err) log.Printf("containr: agent health sweep: %v", err)
return return
} }
for _, r := range agentRows { for rows.Next() {
agents = append(agents, agentHealthRow{ var a agentHealthRow
id: r.ID, name: r.Name, status: r.Status, stale: r.Stale, autoPrune: r.AutoPrune, if err := rows.Scan(&a.id, &a.name, &a.status, &a.stale, &a.autoPrune); err == nil {
}) agents = append(agents, a)
}
} }
rows.Close()
for _, a := range agents { for _, a := range agents {
switch { switch {
case a.stale && a.status != "offline": case a.stale && a.status != "offline":
if err := q.SetAgentStatus(context.Background(), sqlcdb.SetAgentStatusParams{ if _, err := db.Exec(
Status: sql.NullString{String: "offline", Valid: true}, ID: a.id, `UPDATE node_agents SET status = 'offline', updated_at = NOW() WHERE id = $1`, a.id); err != nil {
}); err != nil {
continue continue
} }
notifyAdmins(db, "agent", "Node agent offline", notifyAdmins(db, "agent", "Node agent offline",
fmt.Sprintf("Agent %s missed its heartbeat for over %s.", a.name, staleAfter), "agent", a.id) fmt.Sprintf("Agent %s missed its heartbeat for over %s.", a.name, staleAfter), "agent", a.id)
case !a.stale && a.status == "offline": case !a.stale && a.status == "offline":
if err := q.SetAgentStatus(context.Background(), sqlcdb.SetAgentStatusParams{ if _, err := db.Exec(
Status: sql.NullString{String: "online", Valid: true}, ID: a.id, `UPDATE node_agents SET status = 'online', updated_at = NOW() WHERE id = $1`, a.id); err != nil {
}); err != nil {
continue continue
} }
notifyAdmins(db, "agent", "Node agent back online", notifyAdmins(db, "agent", "Node agent back online",
@@ -183,17 +186,17 @@ func checkForUpgrade(db *database.DB, currentVersion string) {
return return
} }
lastNotified, _ := sqlcdb.New(db.DB).GetAppSetting(context.Background(), "upgrade_notified_tag") var lastNotified string
_ = db.QueryRow(`SELECT value FROM app_settings WHERE key = 'upgrade_notified_tag'`).Scan(&lastNotified)
if lastNotified == release.TagName { if lastNotified == release.TagName {
return return
} }
notifyAdmins(db, "upgrade", "Containr update available", notifyAdmins(db, "upgrade", "Containr update available",
fmt.Sprintf("Release %s is available (running %s). %s", release.TagName, currentVersion, release.HTMLURL), fmt.Sprintf("Release %s is available (running %s). %s", release.TagName, currentVersion, release.HTMLURL),
"release", release.TagName) "release", release.TagName)
_ = sqlcdb.New(db.DB).UpsertAppSettingValue(context.Background(), sqlcdb.UpsertAppSettingValueParams{ _, _ = db.Exec(
Key: "upgrade_notified_tag", `INSERT INTO app_settings (key, value, updated_at) VALUES ('upgrade_notified_tag', $1, NOW())
Value: release.TagName, ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`, release.TagName)
})
} }
// semverNewer reports whether candidate is a higher semver than current. // semverNewer reports whether candidate is a higher semver than current.
+69 -40
View File
@@ -1,16 +1,13 @@
package api package api
import ( import (
"context"
"net/http" "net/http"
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployqueue" "containr/internal/deployqueue"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
// GET /operations — a single cross-cutting view of platform work: active // GET /operations — a single cross-cutting view of platform work: active
@@ -58,59 +55,91 @@ func handleGetOperations(c *gin.Context) {
db := c.MustGet("db").(*database.DB) db := c.MustGet("db").(*database.DB)
isAdmin := contextIsAdmin(c) isAdmin := contextIsAdmin(c)
userUUID, _ := uuid.Parse(userID) // Same visibility rule as the deployment list endpoint.
q := sqlcdb.New(db.DB) visibility := `p.is_approved OR p.owner_id = $1 OR $2::bool
visibility := sqlcdb.ListActiveOperationDeploymentsParams{OwnerID: userUUID, Column2: isAdmin} OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)`
active := []operationDeployment{} active := []operationDeployment{}
if rows, err := q.ListActiveOperationDeployments(context.Background(), visibility); err == nil { rows, err := db.Query(
for _, r := range rows { `SELECT d.id, d.service_id, s.name, p.name, d.status,
active = append(active, operationDeployment{ COALESCE(d.image_name, ''), d.error, d.started_at, d.completed_at, d.created_at
ID: r.ID.String(), ServiceID: r.ServiceID.String(), ServiceName: r.ServiceName, FROM deployments d
ProjectName: r.ProjectName, Status: r.Status.String, Image: r.Image, JOIN services s ON s.id = d.service_id
Error: strPtrNS(r.Error), StartedAt: timePtrNT(r.StartedAt), JOIN projects p ON p.id = s.project_id
CompletedAt: timePtrNT(r.CompletedAt), CreatedAt: r.CreatedAt.Time, WHERE d.status IN ('queued','pending','building','deploying','rolling_back')
}) AND (`+visibility+`)
ORDER BY d.created_at ASC`, userID, isAdmin)
if err == nil {
defer rows.Close()
for rows.Next() {
var d operationDeployment
var sid string
_ = rows.Scan(&d.ID, &sid, &d.ServiceName, &d.ProjectName, &d.Status, &d.Image, &d.Error, &d.StartedAt, &d.CompletedAt, &d.CreatedAt)
d.ServiceID = sid
active = append(active, d)
} }
} }
recentFailed := []operationDeployment{} recentFailed := []operationDeployment{}
if rows, err := q.ListRecentFailedDeployments(context.Background(), sqlcdb.ListRecentFailedDeploymentsParams{ rows2, err2 := db.Query(
OwnerID: userUUID, Column2: isAdmin, `SELECT d.id, d.service_id, s.name, p.name, d.status,
}); err == nil { COALESCE(d.image_name, ''), d.error, d.started_at, d.completed_at, d.created_at
for _, r := range rows { FROM deployments d
recentFailed = append(recentFailed, operationDeployment{ JOIN services s ON s.id = d.service_id
ID: r.ID.String(), ServiceID: r.ServiceID.String(), ServiceName: r.ServiceName, JOIN projects p ON p.id = s.project_id
ProjectName: r.ProjectName, Status: r.Status.String, Image: r.Image, WHERE d.status IN ('failed','cancelled','rolled_back')
Error: strPtrNS(r.Error), StartedAt: timePtrNT(r.StartedAt), AND d.updated_at > NOW() - INTERVAL '24 hours'
CompletedAt: timePtrNT(r.CompletedAt), CreatedAt: r.CreatedAt.Time, AND (`+visibility+`)
}) ORDER BY d.updated_at DESC
LIMIT 20`, userID, isAdmin)
if err2 == nil {
defer rows2.Close()
for rows2.Next() {
var d operationDeployment
var sid string
_ = rows2.Scan(&d.ID, &sid, &d.ServiceName, &d.ProjectName, &d.Status, &d.Image, &d.Error, &d.StartedAt, &d.CompletedAt, &d.CreatedAt)
d.ServiceID = sid
recentFailed = append(recentFailed, d)
} }
} }
cronRuns := []operationCronRun{} cronRuns := []operationCronRun{}
if rows, err := q.ListRecentCronRuns(context.Background(), sqlcdb.ListRecentCronRunsParams{ rows3, err3 := db.Query(
OwnerID: userUUID, Column2: isAdmin, `SELECT e.id, j.name, j.schedule, e.status, e.started_at, e.finished_at, e.error
}); err == nil { FROM cron_executions e
for _, r := range rows { JOIN cron_jobs j ON j.id = e.cron_job_id
cronRuns = append(cronRuns, operationCronRun{ JOIN projects p ON p.id = j.project_id
ID: r.ID.String(), JobName: r.JobName, Schedule: r.Schedule, Status: r.Status.String, WHERE e.started_at > NOW() - INTERVAL '24 hours'
StartedAt: r.StartedAt, FinishedAt: timePtrNT(r.FinishedAt), Error: strPtrNS(r.Error), AND (`+visibility+`)
}) ORDER BY e.started_at DESC
LIMIT 30`, userID, isAdmin)
if err3 == nil {
defer rows3.Close()
for rows3.Next() {
var r operationCronRun
_ = rows3.Scan(&r.ID, &r.JobName, &r.Schedule, &r.Status, &r.StartedAt, &r.FinishedAt, &r.Error)
cronRuns = append(cronRuns, r)
} }
} }
backups := []operationBackup{} backups := []operationBackup{}
if rows, err := q.ListRecentOperationBackups(context.Background(), sqlcdb.ListRecentOperationBackupsParams{ rows4, err4 := db.Query(
UserID: userID, Column2: isAdmin, `SELECT b.id, b.database_id, ds.name, b.status, b.size, b.created_at, b.completed_at
}); err == nil { FROM database_backups b
for _, r := range rows { JOIN database_services ds ON ds.id = b.database_id
backups = append(backups, operationBackup{ WHERE b.created_at > NOW() - INTERVAL '24 hours'
ID: r.ID, DatabaseID: r.DatabaseID, DBName: r.DbName, Status: r.Status, AND (ds.user_id = $1 OR $2::bool)
Size: r.Size, CreatedAt: r.CreatedAt.Time, Completed: timePtrNT(r.CompletedAt), ORDER BY b.created_at DESC
}) LIMIT 20`, userID, isAdmin)
if err4 == nil {
defer rows4.Close()
for rows4.Next() {
var b operationBackup
_ = rows4.Scan(&b.ID, &b.DatabaseID, &b.DBName, &b.Status, &b.Size, &b.CreatedAt, &b.Completed)
backups = append(backups, b)
} }
} }
var queue []deployqueue.ServiceQueueState var queue []deployqueue.ServiceQueueState
if queueValue, exists := c.Get("deploy_queue"); exists && queueValue != nil { if queueValue, exists := c.Get("deploy_queue"); exists && queueValue != nil {
queue = queueValue.(*deployqueue.Queue).Snapshot() queue = queueValue.(*deployqueue.Queue).Snapshot()
@@ -775,7 +775,11 @@ func resolvePreviewHost(db *database.DB, cloneID uuid.UUID, envName string) stri
// loadServiceForDeploy hydrates the Service fields runDeploymentAndSync // loadServiceForDeploy hydrates the Service fields runDeploymentAndSync
// needs — mirrors the loader in handleDeployService. // needs — mirrors the loader in handleDeployService.
func serviceForDeployFromRow(r sqlcdb.GetServiceForDeployRow) Service { 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{ return Service{
ID: r.ID, ProjectID: r.ProjectID, Name: r.Name, Type: r.Type.String, 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, 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, Builder: r.Builder, CPUReserve: r.CpuReserve, MemoryReserve: r.MemoryReserve,
StaticBuildCmd: r.StaticBuildCmd, StaticDir: r.StaticDir, StaticBuildCmd: r.StaticBuildCmd, StaticDir: r.StaticDir,
CreatedAt: r.CreatedAt.Time, UpdatedAt: r.UpdatedAt.Time, CreatedAt: r.CreatedAt.Time, UpdatedAt: r.UpdatedAt.Time,
} }, nil
}
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
} }
// StartPreviewSweeper expires preview environments past their TTL — tears // StartPreviewSweeper expires preview environments past their TTL — tears
+11 -17
View File
@@ -1,12 +1,10 @@
package api package api
import ( import (
"context"
"net/http" "net/http"
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/secrets" "containr/internal/secrets"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -114,8 +112,7 @@ func handleUpdateProjectVariables(c *gin.Context) {
} }
defer tx.Rollback() defer tx.Rollback()
txq := sqlcdb.New(tx) if _, err = tx.Exec(`DELETE FROM project_variables WHERE project_id = $1`, projectID); err != nil {
if err = txq.DeleteProjectVariables(c.Request.Context(), projectID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"})
return return
} }
@@ -130,15 +127,11 @@ func handleUpdateProjectVariables(c *gin.Context) {
if v.IsSecret { if v.IsSecret {
value = secrets.Encrypt(value) value = secrets.Encrypt(value)
} }
if err = txq.InsertProjectVariable(c.Request.Context(), sqlcdb.InsertProjectVariableParams{ if _, err = tx.Exec(
ID: uuid.New(), `INSERT INTO project_variables (id, project_id, key, value, is_secret, created_at, updated_at)
ProjectID: projectID, VALUES ($1, $2, $3, $4, $5, $6, $7)`,
Key: v.Key, uuid.New(), projectID, v.Key, value, v.IsSecret, now, now,
Value: value, ); err != nil {
IsSecret: v.IsSecret,
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key})
return return
} }
@@ -175,10 +168,11 @@ func handleUpdateProjectVariables(c *gin.Context) {
// resolveSharedVar returns the decrypted value of a project variable. // resolveSharedVar returns the decrypted value of a project variable.
func resolveSharedVar(db *database.DB, projectID uuid.UUID, key string) (string, bool) { func resolveSharedVar(db *database.DB, projectID uuid.UUID, key string) (string, bool) {
value, err := sqlcdb.New(db.DB).GetProjectVariableValue(context.Background(), sqlcdb.GetProjectVariableValueParams{ var value string
ProjectID: projectID, err := db.QueryRow(
Key: key, `SELECT value FROM project_variables WHERE project_id = $1 AND key = $2`,
}) projectID, key,
).Scan(&value)
if err != nil { if err != nil {
return "", false return "", false
} }
+7 -9
View File
@@ -1,13 +1,11 @@
package api package api
import ( import (
"context"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/secrets" "containr/internal/secrets"
"github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/registry"
@@ -179,17 +177,17 @@ func handleDeleteRegistry(c *gin.Context) {
// bare image names (nginx:latest). // bare image names (nginx:latest).
func registryAuthFor(db *database.DB, ownerID, imageRef string) registry.AuthConfig { func registryAuthFor(db *database.DB, ownerID, imageRef string) registry.AuthConfig {
host := registryHost(imageRef) host := registryHost(imageRef)
ownerUUID, _ := uuid.Parse(ownerID) var username, password string
row, err := sqlcdb.New(db.DB).GetRegistryAuth(context.Background(), sqlcdb.GetRegistryAuthParams{ err := db.QueryRow(
OwnerID: ownerUUID, `SELECT username, password FROM registries WHERE owner_id = $1 AND host = $2`,
Host: host, ownerID, host,
}) ).Scan(&username, &password)
if err != nil { if err != nil {
return registry.AuthConfig{} return registry.AuthConfig{}
} }
return registry.AuthConfig{ return registry.AuthConfig{
Username: row.Username, Username: username,
Password: secrets.Decrypt(row.Password), Password: secrets.Decrypt(password),
ServerAddress: host, ServerAddress: host,
} }
} }
+28 -23
View File
@@ -11,7 +11,6 @@ import (
"containr/internal/build" "containr/internal/build"
"containr/internal/config" "containr/internal/config"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
"containr/internal/docker" "containr/internal/docker"
@@ -22,7 +21,6 @@ import (
"containr/internal/secrets" "containr/internal/secrets"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg *config.Config) { func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg *config.Config) {
@@ -85,18 +83,19 @@ func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg
// 127.0.0.1 fallback stays for node-bound checks. // 127.0.0.1 fallback stays for node-bound checks.
docker := dockerClient docker := dockerClient
haManager.WithServiceResolver(func(ctx context.Context, serviceID string) (string, int, error) { haManager.WithServiceResolver(func(ctx context.Context, serviceID string) (string, int, error) {
sid, err := uuid.Parse(serviceID) var name, projectID string
var port int
err := db.QueryRowContext(ctx,
`SELECT name, project_id::text, COALESCE(port, 0) FROM services WHERE id = $1`,
serviceID,
).Scan(&name, &projectID, &port)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
svc, err := sqlcdb.New(db.DB).GetServiceNameProjectPort(ctx, sid) if err := docker.ConnectSelfToNetwork(ctx, deployment.ProjectNetworkName(projectID)); err != nil {
if err != nil {
return "", 0, err return "", 0, err
} }
if err := docker.ConnectSelfToNetwork(ctx, deployment.ProjectNetworkName(svc.ProjectID)); err != nil { return name, port, nil
return "", 0, err
}
return svc.Name, int(svc.Port), nil
}) })
} }
haAPIManager := NewHAManager(haManager) haAPIManager := NewHAManager(haManager)
@@ -138,25 +137,31 @@ func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg
// Boot sweep: in-memory engine state dies on restart, so anything that // Boot sweep: in-memory engine state dies on restart, so anything that
// looked in-flight is actually orphaned — mark it failed rather than // looked in-flight is actually orphaned — mark it failed rather than
// leaving phantom 'building'/'queued' rows. // leaving phantom 'building'/'queued' rows.
bootQ := sqlcdb.New(db.DB) _, _ = db.Exec(
_ = bootQ.FailInterruptedDeployments(context.Background()) `UPDATE deployments SET status = 'failed', error = 'Interrupted by server restart', completed_at = NOW(), updated_at = NOW()
WHERE status IN ('queued', 'pending', 'building', 'deploying', 'rolling_back')`,
)
// Boot sweep: encrypt any plaintext secret values left over from before // Boot sweep: encrypt any plaintext secret values left over from before
// encryption-at-rest landed. Transparent and idempotent. // encryption-at-rest landed. Transparent and idempotent.
if encRows, err := bootQ.ListSecretEnvVars(context.Background()); err == nil { if encRows, err := db.Query(`SELECT id, value FROM environment_variables WHERE is_secret = TRUE`); err == nil {
toEncrypt := 0 type encRow struct {
for _, r := range encRows { id string
if secrets.IsEncrypted(r.Value) { value string
continue }
} var toEncrypt []encRow
if err := bootQ.UpdateEnvVarValue(context.Background(), sqlcdb.UpdateEnvVarValueParams{ for encRows.Next() {
Value: secrets.Encrypt(r.Value), ID: r.ID, var r encRow
}); err == nil { if err := encRows.Scan(&r.id, &r.value); err == nil && !secrets.IsEncrypted(r.value) {
toEncrypt++ toEncrypt = append(toEncrypt, r)
} }
} }
if toEncrypt > 0 { encRows.Close()
log.Printf("secrets: encrypted %d plaintext secret variable(s)", toEncrypt) for _, r := range toEncrypt {
_, _ = db.Exec(`UPDATE environment_variables SET value = $1, updated_at = NOW() WHERE id = $2`, secrets.Encrypt(r.value), r.id)
}
if len(toEncrypt) > 0 {
log.Printf("secrets: encrypted %d plaintext secret variable(s)", len(toEncrypt))
} }
} }
+197 -103
View File
@@ -2,7 +2,6 @@ package api
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
@@ -11,7 +10,6 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
"containr/internal/secrets" "containr/internal/secrets"
@@ -24,17 +22,22 @@ import (
// service — covers pinned placements, spread replicas, and leftovers from a // service — covers pinned placements, spread replicas, and leftovers from a
// since-cleared pin. // since-cleared pin.
func remoteAgentsForService(ctx context.Context, db *database.DB, serviceID uuid.UUID) []string { func remoteAgentsForService(ctx context.Context, db *database.DB, serviceID uuid.UUID) []string {
ids, err := sqlcdb.New(db.DB).ListNodeAgentsForService(ctx, serviceID.String()) rows, err := db.Query(
`SELECT DISTINCT node_agent_id FROM container_instances WHERE service_id = $1`,
serviceID,
)
if err != nil { if err != nil {
return nil return nil
} }
out := make([]string, 0, len(ids)) defer rows.Close()
for _, id := range ids { var ids []string
if id != "" { for rows.Next() {
out = append(out, id) var id string
if rows.Scan(&id) == nil && id != "" {
ids = append(ids, id)
} }
} }
return out return ids
} }
// serviceLifecycle routes a start/stop/restart action to every agent holding // serviceLifecycle routes a start/stop/restart action to every agent holding
@@ -96,9 +99,7 @@ func serviceRuntimeSpec(db *database.DB, service Service) (deployment.RuntimeSpe
// Best effort: reuse the host port from the last live deployment so the // Best effort: reuse the host port from the last live deployment so the
// public URL survives restarts. Column exists post-migration; older DBs // public URL survives restarts. Column exists post-migration; older DBs
// silently get ephemeral ports. // silently get ephemeral ports.
if p, err := sqlcdb.New(db.DB).GetServicePublishedPort(context.Background(), service.ID); err == nil { _ = db.QueryRow(`SELECT published_port FROM services WHERE id = $1`, service.ID).Scan(&spec.PublishedPort)
spec.PublishedPort = p
}
if cmd := strings.TrimSpace(service.Command); cmd != "" { if cmd := strings.TrimSpace(service.Command); cmd != "" {
spec.Command = strings.Fields(cmd) spec.Command = strings.Fields(cmd)
} }
@@ -136,23 +137,42 @@ var refVarPattern = regexp.MustCompile(`\$\{\{\s*([A-Za-z0-9_-]+)\.([A-Za-z_][A-
// secrets) and expands ${{service.KEY}} references. KEY may be a variable on // secrets) and expands ${{service.KEY}} references. KEY may be a variable on
// the referenced service, or the builtins HOST (service name alias) and PORT. // the referenced service, or the builtins HOST (service name alias) and PORT.
func resolveServiceEnv(db *database.DB, service Service) (map[string]string, error) { func resolveServiceEnv(db *database.DB, service Service) (map[string]string, error) {
q := sqlcdb.New(db.DB) rows, err := db.Query(
vars, err := q.ListServiceVariableValues(context.Background(), service.ID) `SELECT key, value FROM environment_variables WHERE service_id = $1`,
service.ID,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
env := map[string]string{} env := map[string]string{}
for _, v := range vars { for rows.Next() {
env[v.Key] = secrets.Decrypt(v.Value) var k, v string
if err := rows.Scan(&k, &v); err != nil {
return nil, err
}
env[k] = secrets.Decrypt(v)
} }
refServices, err := q.ListProjectServiceRefs(context.Background(), service.ProjectID) var refServices []struct{ ID, Name string }
svcRows, err := db.Query(
`SELECT id, name FROM services WHERE project_id = $1`,
service.ProjectID,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer svcRows.Close()
for svcRows.Next() {
var s struct{ ID, Name string }
if err := svcRows.Scan(&s.ID, &s.Name); err != nil {
return nil, err
}
refServices = append(refServices, s)
}
byName := map[string]uuid.UUID{} byName := map[string]string{}
for _, s := range refServices { for _, s := range refServices {
byName[strings.ToLower(s.Name)] = s.ID byName[strings.ToLower(s.Name)] = s.ID
} }
@@ -169,15 +189,17 @@ func resolveServiceEnv(db *database.DB, service Service) (map[string]string, err
case "HOST": case "HOST":
return serviceName, true return serviceName, true
case "PORT": case "PORT":
if port, err := q.GetServicePort(context.Background(), id); err == nil && port > 0 { var port int
return strconv.Itoa(int(port)), true if err := db.QueryRow(`SELECT COALESCE(port, 0) FROM services WHERE id = $1`, id).Scan(&port); err == nil && port > 0 {
return strconv.Itoa(port), true
} }
return "", false return "", false
} }
value, err := q.GetServiceVariableValue(context.Background(), sqlcdb.GetServiceVariableValueParams{ var value string
ServiceID: id, err := db.QueryRow(
Key: key, `SELECT value FROM environment_variables WHERE service_id = $1 AND key = $2`,
}) id, key,
).Scan(&value)
if err != nil { if err != nil {
return "", false return "", false
} }
@@ -250,15 +272,6 @@ func parseMemoryLimit(mem string) int64 {
return 0 return 0
} }
// setServiceStatusNow writes the stored status with a fresh updated_at.
func setServiceStatusNow(db *database.DB, id uuid.UUID, status string) {
_ = sqlcdb.New(db.DB).SetServiceStatus(context.Background(), sqlcdb.SetServiceStatusParams{
Status: sql.NullString{String: status, Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: id,
})
}
func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.DB, bool) { func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.DB, bool) {
dbValue, exists := c.Get("db") dbValue, exists := c.Get("db")
if !exists { if !exists {
@@ -273,22 +286,6 @@ func getRuntimeEngine(c *gin.Context) (*deployment.DeploymentEngine, *database.D
return engineValue.(*deployment.DeploymentEngine), dbValue.(*database.DB), true return engineValue.(*deployment.DeploymentEngine), dbValue.(*database.DB), true
} }
// serviceFromRuntimeRow maps the sqlc runtime row (with legacy-column
// fallbacks applied in SQL) onto the API Service model.
func serviceFromRuntimeRow(r sqlcdb.GetServiceRuntimeWithOwnerRow) Service {
return Service{
ID: r.ID, ProjectID: r.ProjectID, Name: r.Name, Type: r.Type,
Status: r.Status, Image: r.Image, Command: r.Command,
Environment: r.Environment, GitRepo: r.GitRepo, GitBranch: r.GitBranch,
BuildPath: r.BuildPath, CPU: r.Cpu, Memory: r.Memory,
Replicas: int(r.Replicas), Port: int(r.Port), Domain: r.Domain,
HealthCheckPath: r.HealthcheckPath, RestartPolicy: r.RestartPolicy,
Builder: r.Builder, CPUReserve: r.CpuReserve,
MemoryReserve: r.MemoryReserve, StaticBuildCmd: r.StaticBuildCmd,
StaticDir: r.StaticDir, CreatedAt: r.CreatedAt.Time, UpdatedAt: r.UpdatedAt.Time,
}
}
func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) { func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) {
serviceID, err := uuid.Parse(c.Param("id")) serviceID, err := uuid.Parse(c.Param("id"))
if err != nil { if err != nil {
@@ -301,16 +298,43 @@ func loadOwnedService(c *gin.Context, db *database.DB) (Service, bool) {
return Service{}, false return Service{}, false
} }
row, err := sqlcdb.New(db.DB).GetServiceRuntimeWithOwner(context.Background(), serviceID) var service Service
var owner string
err = db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(s.memory, ''),
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at, p.owner_id
FROM services s JOIN projects p ON s.project_id = p.id
WHERE s.id = $1`,
serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt, &owner,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false return Service{}, false
} }
if row.OwnerID.String() != userID.(string) && !isAdminUser(db, userID.(string)) { if owner != userID.(string) && !isAdminUser(db, userID.(string)) {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return Service{}, false return Service{}, false
} }
return serviceFromRuntimeRow(row), true return service, true
} }
// loadReadableService loads a service when the caller may read its project: // loadReadableService loads a service when the caller may read its project:
@@ -322,13 +346,39 @@ func loadReadableService(c *gin.Context, db *database.DB) (Service, bool) {
return Service{}, false return Service{}, false
} }
row, err := sqlcdb.New(db.DB).GetServiceRuntimeWithOwner(context.Background(), serviceID) var service Service
var projectID uuid.UUID
err = db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(s.memory, ''),
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at, s.project_id
FROM services s
WHERE s.id = $1`,
serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt, &projectID,
)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false return Service{}, false
} }
service := serviceFromRuntimeRow(row) if _, allowed := projectReadAccess(c, db, projectID); !allowed {
if _, allowed := projectReadAccess(c, db, service.ProjectID); !allowed {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return Service{}, false return Service{}, false
} }
@@ -379,7 +429,7 @@ func reconcileNow(c *gin.Context, engine *deployment.DeploymentEngine, db *datab
status = state.Status status = state.Status
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
} }
setServiceStatusNow(db, service.ID, status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, status, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"runtime": state}) c.JSON(http.StatusOK, gin.H{"runtime": state})
return true return true
} }
@@ -398,7 +448,7 @@ func handleServiceStart(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "running") _, _ = db.Exec(`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "running"}) c.JSON(http.StatusOK, gin.H{"status": "running"})
} }
@@ -416,7 +466,7 @@ func handleServiceStop(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "stopped") _, _ = db.Exec(`UPDATE services SET status = 'stopped', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "stopped"}) c.JSON(http.StatusOK, gin.H{"status": "stopped"})
} }
@@ -435,7 +485,7 @@ func handleServiceRestart(c *gin.Context) {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
setServiceStatusNow(db, service.ID, "running") _, _ = db.Exec(`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
c.JSON(http.StatusOK, gin.H{"status": "running"}) c.JSON(http.StatusOK, gin.H{"status": "running"})
} }
@@ -455,8 +505,12 @@ func handleServiceRedeploy(c *gin.Context) {
image := service.Image image := service.Image
if image == "" { if image == "" {
// Git-sourced service: reuse the last successful deployment image. // Git-sourced service: reuse the last successful deployment image.
var err error err := db.QueryRow(
image, err = sqlcdb.New(db.DB).GetLastDeployedImage(context.Background(), service.ID) `SELECT image_name || ':' || image_tag FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`,
service.ID,
).Scan(&image)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No deployable image yet — run a deployment first"}) c.JSON(http.StatusBadRequest, gin.H{"error": "No deployable image yet — run a deployment first"})
return return
@@ -502,7 +556,7 @@ func reconcileServiceJob(ctx context.Context, db *database.DB, engine *deploymen
if state.Status != "" { if state.Status != "" {
status = state.Status status = state.Status
} }
setServiceStatusNow(db, service.ID, status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, status, time.Now(), service.ID)
} }
return state, nil return state, nil
} }
@@ -521,9 +575,12 @@ func enqueueServiceRedeploy(c *gin.Context, db *database.DB, service Service) bo
} }
image := service.Image image := service.Image
if image == "" { if image == "" {
var err error if err := db.QueryRow(
image, err = sqlcdb.New(db.DB).GetLastDeployedImage(context.Background(), service.ID) `SELECT image_name || ':' || image_tag FROM deployments
if err != nil { WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`,
service.ID,
).Scan(&image); err != nil {
return false return false
} }
} }
@@ -571,10 +628,10 @@ func persistPublishedPort(db *database.DB, serviceID uuid.UUID, ports []uint16)
if len(ports) == 0 { if len(ports) == 0 {
return return
} }
_ = sqlcdb.New(db.DB).SetServicePublishedPort(context.Background(), sqlcdb.SetServicePublishedPortParams{ _, _ = db.Exec(
PublishedPort: int32(ports[0]), `UPDATE services SET published_port = $1 WHERE id = $2 AND published_port <> $1`,
ID: serviceID, int(ports[0]), serviceID,
}) )
} }
// liveServiceStatus reconciles the stored status with real container state. // liveServiceStatus reconciles the stored status with real container state.
@@ -597,7 +654,7 @@ func liveServiceStatus(c *gin.Context, db *database.DB, service *Service) {
} }
if state.Status != service.Status { if state.Status != service.Status {
service.Status = state.Status service.Status = state.Status
setServiceStatusNow(db, service.ID, state.Status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, state.Status, time.Now(), service.ID)
} }
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
if service.Domain != "" { if service.Domain != "" {
@@ -619,22 +676,44 @@ func runtimeScaleService(c *gin.Context, serviceID string, replicas int) error {
return fmt.Errorf("runtime unavailable") return fmt.Errorf("runtime unavailable")
} }
q := sqlcdb.New(db.DB) var service Service
sid, err := uuid.Parse(serviceID) var owner string
err := db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(s.memory, ''),
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at, p.owner_id
FROM services s JOIN projects p ON s.project_id = p.id
WHERE s.id = $1`, serviceID,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt, &owner,
)
if err != nil { if err != nil {
return err return err
} }
row, err := q.GetServiceRuntimeWithOwner(context.Background(), sid)
if err != nil {
return err
}
service := serviceFromRuntimeRow(row)
if service.Image == "" { if service.Image == "" {
image, err := q.GetLastDeployedImage(context.Background(), service.ID) if err := db.QueryRow(
if err != nil { `SELECT image_name || ':' || image_tag FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1`, service.ID,
).Scan(&service.Image); err != nil {
return fmt.Errorf("no deployed image to scale") return fmt.Errorf("no deployed image to scale")
} }
service.Image = image
} }
spec, err := serviceRuntimeSpec(db, service) spec, err := serviceRuntimeSpec(db, service)
@@ -652,11 +731,7 @@ func runtimeScaleService(c *gin.Context, serviceID string, replicas int) error {
if state != nil { if state != nil {
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
} }
_ = q.SetServiceReplicas(context.Background(), sqlcdb.SetServiceReplicasParams{ _, _ = db.Exec(`UPDATE services SET replicas = $1, updated_at = $2 WHERE id = $3`, replicas, time.Now(), service.ID)
Replicas: int32(replicas),
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: service.ID,
})
return nil return nil
} }
@@ -675,34 +750,47 @@ func handleServiceEnvCheck(c *gin.Context) {
return return
} }
q := sqlcdb.New(db.DB) rows, err := db.Query(
vars, err := q.ListServiceVariablesWithSecret(context.Background(), service.ID) `SELECT key, value, COALESCE(is_secret, false) FROM environment_variables WHERE service_id = $1`,
service.ID,
)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
defer rows.Close()
var unresolved, empty, unreadable []string var unresolved, empty, unreadable []string
var raw []struct{ key, val string } var raw []struct{ key, val string }
for _, v := range vars { for rows.Next() {
decrypted := secrets.Decrypt(v.Value) var k, v string
if v.IsSecret && secrets.IsEncrypted(v.Value) && decrypted == v.Value { var secret bool
if err := rows.Scan(&k, &v, &secret); err != nil {
continue
}
decrypted := secrets.Decrypt(v)
if secret && secrets.IsEncrypted(v) && decrypted == v {
// Ciphertext failed to decrypt — Decrypt returns the input. // Ciphertext failed to decrypt — Decrypt returns the input.
unreadable = append(unreadable, v.Key) unreadable = append(unreadable, k)
continue continue
} }
if strings.TrimSpace(decrypted) == "" { if strings.TrimSpace(decrypted) == "" {
empty = append(empty, v.Key) empty = append(empty, k)
} }
raw = append(raw, struct{ key, val string }{v.Key, decrypted}) raw = append(raw, struct{ key, val string }{k, decrypted})
} }
// Project siblings for ${{name.KEY}} resolution. // Project siblings for ${{name.KEY}} resolution.
siblingIDs := map[string]uuid.UUID{} siblingIDs := map[string]string{}
if refs, err := q.ListProjectServiceRefs(context.Background(), service.ProjectID); err == nil { svcRows, err := db.Query(`SELECT id, name FROM services WHERE project_id = $1`, service.ProjectID)
for _, s := range refs { if err == nil {
siblingIDs[strings.ToLower(s.Name)] = s.ID for svcRows.Next() {
var id, name string
if err := svcRows.Scan(&id, &name); err == nil {
siblingIDs[strings.ToLower(name)] = id
}
} }
svcRows.Close()
} }
resolvable := func(serviceName, key string) bool { resolvable := func(serviceName, key string) bool {
@@ -714,14 +802,20 @@ func handleServiceEnvCheck(c *gin.Context) {
return true return true
} }
if strings.EqualFold(key, "PORT") { if strings.EqualFold(key, "PORT") {
port, err := q.GetServicePort(context.Background(), id) var port int
return err == nil && port > 0 if err := db.QueryRow(`SELECT COALESCE(port, 0) FROM services WHERE id = $1`, id).Scan(&port); err == nil {
return port > 0
}
return false
} }
_, err := q.GetServiceVariableValue(context.Background(), sqlcdb.GetServiceVariableValueParams{ var n int
ServiceID: id, if err := db.QueryRow(
Key: key, `SELECT COUNT(*) FROM environment_variables WHERE service_id = $1 AND key = $2`,
}) id, key,
return err == nil ).Scan(&n); err == nil {
return n > 0
}
return false
} }
for _, kv := range raw { for _, kv := range raw {
+109 -70
View File
@@ -2,9 +2,7 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/security" "containr/internal/security"
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -35,8 +33,8 @@ func NewSecurityHandler(db *database.DB, encryptionKey string) *SecurityHandler
// Notify the project owner when a background scan completes — criticals // Notify the project owner when a background scan completes — criticals
// earn a distinct title so they stand out in the notification list. // earn a distinct title so they stand out in the notification list.
scanner.OnScanComplete = func(scan *security.SecurityScan) { scanner.OnScanComplete = func(scan *security.SecurityScan) {
ownerID, err := sqlcdb.New(db.DB).GetProjectOwnerText(context.Background(), uuid.MustParse(scan.ProjectID)) var ownerID string
if err != nil || ownerID == "" { if err := db.QueryRow(`SELECT owner_id::text FROM projects WHERE id = $1`, scan.ProjectID).Scan(&ownerID); err != nil || ownerID == "" {
return return
} }
title := "Security scan completed" title := "Security scan completed"
@@ -83,10 +81,14 @@ func (sh *SecurityHandler) StartSecurityScan(c *gin.Context) {
return return
} }
serviceExists, err := sqlcdb.New(sh.db.DB).ServiceExistsInProject(context.Background(), sqlcdb.ServiceExistsInProjectParams{ var serviceExists bool
ID: uuid.MustParse(req.ServiceID), err := sh.db.QueryRow(
ProjectID: uuid.MustParse(req.ProjectID), `SELECT EXISTS(
}) SELECT 1 FROM services WHERE id = $1 AND project_id = $2
)`,
req.ServiceID,
req.ProjectID,
).Scan(&serviceExists)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate service"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate service"})
return return
@@ -162,30 +164,40 @@ func (sh *SecurityHandler) GetVulnerabilities(c *gin.Context) {
} }
// Query vulnerabilities // Query vulnerabilities
rows, err := sqlcdb.New(sh.db.DB).ListVulnerabilities(context.Background(), uuid.MustParse(projectID)) rows, err := sh.db.Query(`
SELECT id, type, severity, title, description, service_id, status, found_at, resolved_at
FROM vulnerabilities
WHERE project_id = $1
ORDER BY
CASE severity
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
found_at DESC
`, projectID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerabilities"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerabilities"})
return return
} }
defer rows.Close()
var vulnerabilities []security.Vulnerability var vulnerabilities []security.Vulnerability
for _, row := range rows { for rows.Next() {
// NULL description/service_id failed the original scan and dropped var vuln security.Vulnerability
// the row — keep the same filter. var resolvedAt *time.Time
if !row.Description.Valid || !row.ServiceID.Valid {
err := rows.Scan(&vuln.ID, &vuln.Type, &vuln.Severity, &vuln.Title, &vuln.Description,
&vuln.ServiceID, &vuln.Status, &vuln.FoundAt, &resolvedAt)
if err != nil {
continue continue
} }
vulnerabilities = append(vulnerabilities, security.Vulnerability{
ID: row.ID.String(), vuln.ResolvedAt = resolvedAt
Type: row.Type, vulnerabilities = append(vulnerabilities, vuln)
Severity: row.Severity,
Title: row.Title,
Description: row.Description.String,
ServiceID: row.ServiceID.UUID.String(),
Status: row.Status,
FoundAt: row.FoundAt,
ResolvedAt: timePtrNT(row.ResolvedAt),
})
} }
c.JSON(http.StatusOK, gin.H{"vulnerabilities": vulnerabilities}) c.JSON(http.StatusOK, gin.H{"vulnerabilities": vulnerabilities})
@@ -215,11 +227,11 @@ func (sh *SecurityHandler) UpdateVulnerability(c *gin.Context) {
resolvedAt = &now resolvedAt = &now
} }
err := sqlcdb.New(sh.db.DB).UpdateVulnerabilityStatus(context.Background(), sqlcdb.UpdateVulnerabilityStatusParams{ _, err := sh.db.Exec(`
Status: req.Status, UPDATE vulnerabilities
ResolvedAt: ntPtr(resolvedAt), SET status = $1, resolved_at = $2
ID: uuid.MustParse(vulnID), WHERE id = $3
}) `, req.Status, resolvedAt, vulnID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vulnerability"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vulnerability"})
@@ -259,7 +271,13 @@ func (sh *SecurityHandler) StartComplianceAssessment(c *gin.Context) {
return return
} }
frameworkExists, err := sqlcdb.New(sh.db.DB).ComplianceFrameworkExists(context.Background(), uuid.MustParse(req.FrameworkID)) var frameworkExists bool
err := sh.db.QueryRow(
`SELECT EXISTS(
SELECT 1 FROM compliance_frameworks WHERE id = $1
)`,
req.FrameworkID,
).Scan(&frameworkExists)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate framework"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate framework"})
return return
@@ -303,25 +321,30 @@ func (sh *SecurityHandler) GetComplianceReport(c *gin.Context) {
// GetComplianceFrameworks retrieves available compliance frameworks // GetComplianceFrameworks retrieves available compliance frameworks
func (sh *SecurityHandler) GetComplianceFrameworks(c *gin.Context) { func (sh *SecurityHandler) GetComplianceFrameworks(c *gin.Context) {
rows, err := sqlcdb.New(sh.db.DB).ListComplianceFrameworks(context.Background()) rows, err := sh.db.Query(`
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name
`)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance frameworks"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance frameworks"})
return return
} }
defer rows.Close()
var frameworks []security.ComplianceFramework var frameworks []security.ComplianceFramework
for _, row := range rows { for rows.Next() {
if !row.Description.Valid { var framework security.ComplianceFramework
err := rows.Scan(&framework.ID, &framework.Name, &framework.Description,
&framework.Version, &framework.Enabled, &framework.CreatedAt)
if err != nil {
continue continue
} }
frameworks = append(frameworks, security.ComplianceFramework{
ID: row.ID.String(), frameworks = append(frameworks, framework)
Name: row.Name,
Description: row.Description.String,
Version: row.Version,
Enabled: row.Enabled,
CreatedAt: row.CreatedAt,
})
} }
c.JSON(http.StatusOK, gin.H{"frameworks": frameworks}) c.JSON(http.StatusOK, gin.H{"frameworks": frameworks})
@@ -362,19 +385,23 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
Resolved int `json:"resolved"` Resolved int `json:"resolved"`
} }
q := sqlcdb.New(sh.db.DB) err := sh.db.QueryRow(`
mrow, err := q.GetVulnerabilityMetrics(context.Background(), uuid.MustParse(projectID)) SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE severity = 'critical') as critical,
COUNT(*) FILTER (WHERE severity = 'high') as high,
COUNT(*) FILTER (WHERE severity = 'medium') as medium,
COUNT(*) FILTER (WHERE severity = 'low') as low,
COUNT(*) FILTER (WHERE status = 'open') as open,
COUNT(*) FILTER (WHERE status = 'resolved') as resolved
FROM vulnerabilities
WHERE project_id = $1
`, projectID).Scan(&vulnMetrics.Total, &vulnMetrics.Critical, &vulnMetrics.High,
&vulnMetrics.Medium, &vulnMetrics.Low, &vulnMetrics.Open, &vulnMetrics.Resolved)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerability metrics"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerability metrics"})
return return
} }
vulnMetrics.Total = int(mrow.Total)
vulnMetrics.Critical = int(mrow.Critical)
vulnMetrics.High = int(mrow.High)
vulnMetrics.Medium = int(mrow.Medium)
vulnMetrics.Low = int(mrow.Low)
vulnMetrics.Open = int(mrow.Open)
vulnMetrics.Resolved = int(mrow.Resolved)
// Get latest scan // Get latest scan
var latestScan struct { var latestScan struct {
@@ -384,11 +411,13 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
Status string `json:"status"` Status string `json:"status"`
} }
srow, err := q.GetLatestSecurityScan(context.Background(), uuid.MustParse(projectID)) err = sh.db.QueryRow(`
latestScan.ID = srow.ID.String() SELECT id, COALESCE((summary->>'score')::int, 0), started_at as scanned_at, status
latestScan.Score = int(srow.Score) FROM security_scans
latestScan.ScannedAt = srow.ScannedAt WHERE project_id = $1
latestScan.Status = srow.Status ORDER BY started_at DESC
LIMIT 1
`, projectID).Scan(&latestScan.ID, &latestScan.Score, &latestScan.ScannedAt, &latestScan.Status)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
latestScan = struct { latestScan = struct {
@@ -409,10 +438,13 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
LastAssessed *time.Time `json:"last_assessed"` LastAssessed *time.Time `json:"last_assessed"`
} }
crow, err := q.GetLatestComplianceReport(context.Background(), uuid.MustParse(projectID)) err = sh.db.QueryRow(`
complianceStatus.OverallStatus = crow.OverallStatus SELECT overall_status, score, assessment_date
complianceStatus.Score = int(crow.Score) FROM compliance_reports
complianceStatus.LastAssessed = &crow.AssessmentDate WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1
`, projectID).Scan(&complianceStatus.OverallStatus, &complianceStatus.Score, &complianceStatus.LastAssessed)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
complianceStatus = struct { complianceStatus = struct {
@@ -582,11 +614,21 @@ func (sh *SecurityHandler) requireProjectAccess(c *gin.Context, projectID string
return "", false return "", false
} }
hasAccess, err := sqlcdb.New(sh.db.DB).CheckSecurityProjectAccess(context.Background(), sqlcdb.CheckSecurityProjectAccessParams{ var hasAccess bool
ID: uuid.MustParse(projectID), err := sh.db.QueryRow(
OwnerID: optionalUserUUID(c), `SELECT EXISTS (
Column3: contextIsAdmin(c), SELECT 1
}) FROM projects p
WHERE p.id = $1
AND (p.is_approved
OR $3::bool
OR p.owner_id = $2 OR EXISTS (
SELECT 1 FROM project_members pm
WHERE pm.project_id = p.id AND pm.user_id = $2
))
)`,
projectID, optionalUserUUID(c), contextIsAdmin(c),
).Scan(&hasAccess)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify project access"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify project access"})
return "", false return "", false
@@ -607,8 +649,7 @@ func (sh *SecurityHandler) requireSecurityScanAccess(c *gin.Context, scanID stri
} }
var projectID string var projectID string
pid, err := sqlcdb.New(sh.db.DB).GetSecurityScanProjectID(context.Background(), uuid.MustParse(scanID)) err := sh.db.QueryRow("SELECT project_id FROM security_scans WHERE id = $1", scanID).Scan(&projectID)
projectID = pid.String()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"})
return false return false
@@ -629,8 +670,7 @@ func (sh *SecurityHandler) requireComplianceReportAccess(c *gin.Context, reportI
} }
var projectID string var projectID string
pid, err := sqlcdb.New(sh.db.DB).GetComplianceReportProjectID(context.Background(), uuid.MustParse(reportID)) err := sh.db.QueryRow("SELECT project_id FROM compliance_reports WHERE id = $1", reportID).Scan(&projectID)
projectID = pid.String()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"})
return false return false
@@ -651,8 +691,7 @@ func (sh *SecurityHandler) requireVulnerabilityAccess(c *gin.Context, vulnID str
} }
var projectID string var projectID string
pid, err := sqlcdb.New(sh.db.DB).GetVulnerabilityProjectID(context.Background(), uuid.MustParse(vulnID)) err := sh.db.QueryRow("SELECT project_id FROM vulnerabilities WHERE id = $1", vulnID).Scan(&projectID)
projectID = pid.String()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Vulnerability not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Vulnerability not found"})
return "", false return "", false
-212
View File
@@ -1,212 +0,0 @@
package api
import (
"context"
"database/sql"
"strings"
"testing"
"time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
)
// Live invariant test against real Postgres — set METRICS_TEST_DSN (or
// DATABASE_URL). Covers the generated auth/security queries: user upsert
// conflict path, project-access EXISTS predicate, vulnerability ordering and
// status update, latest-scan/report lookups, and the framework list.
func TestSecurityLiveInvariants(t *testing.T) {
dsn := testDSNString(t)
db, err := database.NewConnection(dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer db.Close()
ctx := context.Background()
q := sqlcdb.New(db.DB)
userID := uuid.New()
otherID := uuid.New()
projectID := uuid.New()
envID := uuid.New()
serviceID := uuid.New()
scanID := uuid.New()
reportID := uuid.New()
frameworkID := uuid.New()
vulnCrit := uuid.New()
vulnLow := uuid.New()
suffix := strings.ReplaceAll(userID.String()[:8], "-", "")
must := func(err error) {
t.Helper()
if err != nil {
t.Fatalf("setup: %v", err)
}
}
_, err = db.Exec(`INSERT INTO users (id, email, password_hash, name) VALUES ($1, $2, 'x', 'test'), ($3, $4, 'x', 'other')`,
userID, "sec-"+suffix+"@test.local", otherID, "sec-"+suffix+"[email protected]")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, false)`,
projectID, "sec-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type)
VALUES ($1, $2, $3, $4, 'web', 'image')`,
serviceID, "sec-"+suffix, projectID, envID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id IN ($1, $2)`, userID, otherID) }()
// Auth queries: upsert conflicts update rather than error, counts see rows.
_, err = q.InsertUserAdmin(ctx, sqlcdb.InsertUserAdminParams{
Email: "ins-" + suffix + "@test.local", PasswordHash: "x", Name: "ins", IsAdmin: false,
})
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE email = $1`, "ins-"+suffix+"@test.local") }()
urow, err := q.UpsertLocalUser(ctx, sqlcdb.UpsertLocalUserParams{
Email: "ins-" + suffix + "@test.local", PasswordHash: "y", Name: "renamed",
Column4: "http://img", IsAdmin: false,
})
must(err)
if urow.Name != "renamed" || urow.AvatarUrl != "http://img" {
t.Fatalf("upsert: %+v", urow)
}
byEmail, err := q.GetUserByEmailForAuth(ctx, "ins-"+suffix+"@test.local")
must(err)
if byEmail.ID != urow.ID || byEmail.PasswordHash != "x" {
t.Fatalf("by email: %+v", byEmail)
}
count, err := q.CountUsersByEmail(ctx, "ins-"+suffix+"@test.local")
must(err)
if count != 1 {
t.Fatalf("count by email: %d", count)
}
byID, err := q.GetUserByID(ctx, urow.ID)
must(err)
if byID.Email != "ins-"+suffix+"@test.local" {
t.Fatalf("by id: %+v", byID)
}
// Access predicate: unapproved project is owner/admin/member only.
ownerAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: userID, Column3: false,
})
must(err)
if !ownerAccess {
t.Fatal("owner should have access")
}
strangerAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: otherID, Column3: false,
})
must(err)
if strangerAccess {
t.Fatal("stranger should not have access")
}
adminAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: otherID, Column3: true,
})
must(err)
if !adminAccess {
t.Fatal("admin should have access")
}
ownerText, err := q.GetProjectOwnerText(ctx, projectID)
must(err)
if ownerText != userID.String() {
t.Fatalf("owner text: %q", ownerText)
}
exists, err := q.ServiceExistsInProject(ctx, sqlcdb.ServiceExistsInProjectParams{
ID: serviceID, ProjectID: projectID,
})
must(err)
if !exists {
t.Fatal("service should exist in project")
}
// Scan/report/vuln rows via raw inserts (fixtures), reads via sqlc.
_, err = db.Exec(`INSERT INTO security_scans (id, project_id, service_id, scan_type, status, summary, started_at)
VALUES ($1, $2, $3, 'dependency', 'completed', '{"score": 85}', NOW() - interval '1 hour')`,
scanID, projectID, serviceID)
must(err)
_, err = db.Exec(`INSERT INTO compliance_frameworks (id, name, description, version, enabled)
VALUES ($1, $2, 'desc', '1.0', true)`,
frameworkID, "fw-"+suffix)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM compliance_frameworks WHERE id = $1`, frameworkID) }()
_, err = db.Exec(`INSERT INTO compliance_reports (id, project_id, framework_id, overall_status, score, assessment_date)
VALUES ($1, $2, $3, 'compliant', 92, NOW() - interval '30 minutes')`,
reportID, projectID, frameworkID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM compliance_reports WHERE id = $1`, reportID) }()
_, err = db.Exec(`INSERT INTO vulnerabilities (id, type, severity, title, description, service_id, project_id, status, found_at)
VALUES ($1, 'dependency', 'low', 'low-v', 'd', $3, $4, 'open', NOW() - interval '2 hours'),
($2, 'dependency', 'critical', 'crit-v', 'd', $3, $4, 'open', NOW() - interval '1 hour')`,
vulnLow, vulnCrit, serviceID, projectID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM vulnerabilities WHERE project_id = $1`, projectID) }()
defer func() { _, _ = db.Exec(`DELETE FROM security_scans WHERE id = $1`, scanID) }()
// Severity ordering: critical first.
vulns, err := q.ListVulnerabilities(ctx, projectID)
must(err)
if len(vulns) != 2 || vulns[0].ID != vulnCrit {
t.Fatalf("vuln ordering: %+v", vulns)
}
metrics, err := q.GetVulnerabilityMetrics(ctx, projectID)
must(err)
if metrics.Total != 2 || metrics.Critical != 1 || metrics.Low != 1 || metrics.Open != 2 {
t.Fatalf("metrics: %+v", metrics)
}
must(q.UpdateVulnerabilityStatus(ctx, sqlcdb.UpdateVulnerabilityStatusParams{
Status: "resolved",
ResolvedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: vulnCrit,
}))
metrics, err = q.GetVulnerabilityMetrics(ctx, projectID)
must(err)
if metrics.Open != 1 || metrics.Resolved != 1 {
t.Fatalf("metrics after resolve: %+v", metrics)
}
// Latest scan/report lookups.
scan, err := q.GetLatestSecurityScan(ctx, projectID)
must(err)
if scan.ID != scanID || scan.Score != 85 || scan.Status != "completed" {
t.Fatalf("latest scan: %+v", scan)
}
report, err := q.GetLatestComplianceReport(ctx, projectID)
must(err)
if report.OverallStatus != "compliant" || report.Score != 92 {
t.Fatalf("latest report: %+v", report)
}
// Project-ID lookups used by the access guards.
scanPID, err := q.GetSecurityScanProjectID(ctx, scanID)
must(err)
reportPID, err := q.GetComplianceReportProjectID(ctx, reportID)
must(err)
vulnPID, err := q.GetVulnerabilityProjectID(ctx, vulnCrit)
must(err)
if scanPID != projectID || reportPID != projectID || vulnPID != projectID {
t.Fatalf("project id lookups: %v %v %v", scanPID, reportPID, vulnPID)
}
// Framework list only returns enabled rows.
frameworks, err := q.ListComplianceFrameworks(ctx)
must(err)
found := false
for _, f := range frameworks {
if f.ID == frameworkID && f.Enabled {
found = true
}
}
if !found {
t.Fatal("enabled framework missing from list")
}
frameworkExists, err := q.ComplianceFrameworkExists(ctx, frameworkID)
must(err)
if !frameworkExists {
t.Fatal("framework should exist")
}
}
+29 -22
View File
@@ -2,7 +2,6 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/deployment" "containr/internal/deployment"
"containr/internal/deployqueue" "containr/internal/deployqueue"
"context" "context"
@@ -71,18 +70,20 @@ type Service struct {
// loadServiceSleep fills the sleep-mode columns, which live outside the // loadServiceSleep fills the sleep-mode columns, which live outside the
// wide service SELECTs. Missing columns (pre-migration) degrade to off. // wide service SELECTs. Missing columns (pre-migration) degrade to off.
func loadServiceSleep(db *database.DB, s *Service) { func loadServiceSleep(db *database.DB, s *Service) {
row, err := sqlcdb.New(db.DB).GetServiceSleep(context.Background(), s.ID) var idle int
if err == nil { if err := db.QueryRow(
s.SleepEnabled = row.SleepEnabled `SELECT COALESCE(sleep_enabled, false), COALESCE(sleep_idle_minutes, 15) FROM services WHERE id = $1`,
s.SleepIdleMinutes = int(row.SleepIdleMinutes) s.ID,
).Scan(&s.SleepEnabled, &idle); err == nil {
s.SleepIdleMinutes = idle
} }
} }
// serviceNodeID reads the pin column directly — kept out of the wide service // serviceNodeID reads the pin column directly — kept out of the wide service
// SELECTs so pre-migration schemas keep working. // SELECTs so pre-migration schemas keep working.
func serviceNodeID(db *database.DB, serviceID uuid.UUID) string { func serviceNodeID(db *database.DB, serviceID uuid.UUID) string {
id, err := sqlcdb.New(db.DB).GetServiceNodeID(context.Background(), serviceID) var id sql.NullString
if err == nil { if err := db.QueryRow(`SELECT node_id FROM services WHERE id = $1`, serviceID).Scan(&id); err == nil {
return id.String return id.String
} }
return "" return ""
@@ -90,14 +91,17 @@ func serviceNodeID(db *database.DB, serviceID uuid.UUID) string {
// serviceSpread reads the spread column with the same lazy pattern. // serviceSpread reads the spread column with the same lazy pattern.
func serviceSpread(db *database.DB, serviceID uuid.UUID) bool { func serviceSpread(db *database.DB, serviceID uuid.UUID) bool {
spread, err := sqlcdb.New(db.DB).GetServiceSpread(context.Background(), serviceID) var spread bool
return err == nil && spread if err := db.QueryRow(`SELECT COALESCE(spread, false) FROM services WHERE id = $1`, serviceID).Scan(&spread); err == nil {
return spread
}
return false
} }
// servicePlacementTags reads the placement_tags column with the lazy pattern. // servicePlacementTags reads the placement_tags column with the lazy pattern.
func servicePlacementTags(db *database.DB, serviceID uuid.UUID) []string { func servicePlacementTags(db *database.DB, serviceID uuid.UUID) []string {
raw, err := sqlcdb.New(db.DB).GetServicePlacementTags(context.Background(), serviceID) var raw []byte
if err != nil { if err := db.QueryRow(`SELECT placement_tags FROM services WHERE id = $1`, serviceID).Scan(&raw); err != nil {
return nil return nil
} }
var tags []string var tags []string
@@ -175,8 +179,8 @@ func normalizeTraefikLabels(raw map[string]string) (map[string]string, error) {
// serviceTraefikLabels lazy-reads the traefik_labels column. // serviceTraefikLabels lazy-reads the traefik_labels column.
func serviceTraefikLabels(db *database.DB, serviceID uuid.UUID) map[string]string { func serviceTraefikLabels(db *database.DB, serviceID uuid.UUID) map[string]string {
raw, err := sqlcdb.New(db.DB).GetServiceTraefikLabels(context.Background(), serviceID) var raw []byte
if err != nil { if err := db.QueryRow(`SELECT traefik_labels FROM services WHERE id = $1`, serviceID).Scan(&raw); err != nil {
return nil return nil
} }
var labels map[string]string var labels map[string]string
@@ -207,6 +211,7 @@ func tagsJSON(tags []string) []byte {
// loadServiceNode fills the pin + spread + placement tags + the agent's // loadServiceNode fills the pin + spread + placement tags + the agent's
// display name for responses. // display name for responses.
func loadServiceNode(db *database.DB, s *Service) { func loadServiceNode(db *database.DB, s *Service) {
var name sql.NullString
s.NodeID = serviceNodeID(db, s.ID) s.NodeID = serviceNodeID(db, s.ID)
s.Spread = serviceSpread(db, s.ID) s.Spread = serviceSpread(db, s.ID)
s.PlacementTags = servicePlacementTags(db, s.ID) s.PlacementTags = servicePlacementTags(db, s.ID)
@@ -214,8 +219,8 @@ func loadServiceNode(db *database.DB, s *Service) {
if s.NodeID == "" { if s.NodeID == "" {
return return
} }
if name, err := sqlcdb.New(db.DB).GetNodeAgentName(context.Background(), s.NodeID); err == nil { if err := db.QueryRow(`SELECT name FROM node_agents WHERE id = $1`, s.NodeID).Scan(&name); err == nil {
s.NodeName = name s.NodeName = name.String
} }
} }
@@ -230,8 +235,8 @@ type ServiceVolume struct {
// loadServiceVolumes reads the JSONB column; absent/invalid data degrades // loadServiceVolumes reads the JSONB column; absent/invalid data degrades
// to no mounts rather than failing the request. // to no mounts rather than failing the request.
func loadServiceVolumes(db *database.DB, serviceID uuid.UUID) []deployment.VolumeMount { func loadServiceVolumes(db *database.DB, serviceID uuid.UUID) []deployment.VolumeMount {
raw, err := sqlcdb.New(db.DB).GetServiceVolumes(context.Background(), serviceID) var raw []byte
if err != nil { if err := db.QueryRow(`SELECT volumes FROM services WHERE id = $1`, serviceID).Scan(&raw); err != nil {
return nil return nil
} }
var stored []ServiceVolume var stored []ServiceVolume
@@ -698,11 +703,13 @@ func handleCreateService(c *gin.Context) {
} }
func getProjectEnvironmentID(db *database.DB, projectID uuid.UUID, environment string) (uuid.UUID, error) { func getProjectEnvironmentID(db *database.DB, projectID uuid.UUID, environment string) (uuid.UUID, error) {
row, err := sqlcdb.New(db.DB).GetProjectEnvironmentByName(context.Background(), sqlcdb.GetProjectEnvironmentByNameParams{ var environmentID uuid.UUID
ProjectID: projectID, err := db.QueryRow(
Name: environment, "SELECT id FROM environments WHERE project_id = $1 AND name = $2",
}) projectID,
return row.ID, err environment,
).Scan(&environmentID)
return environmentID, err
} }
func inferServiceSourceType(service Service) string { func inferServiceSourceType(service Service) string {
+16 -14
View File
@@ -10,13 +10,11 @@ import (
"time" "time"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/docker" "containr/internal/docker"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/registry"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
// Platform setting keys. Values stored in app_settings take precedence over // Platform setting keys. Values stored in app_settings take precedence over
@@ -72,7 +70,8 @@ func handleGetBranding(c *gin.Context) {
// then the caller's default. // then the caller's default.
func settingValue(db *database.DB, key, envKey, fallback string) string { func settingValue(db *database.DB, key, envKey, fallback string) string {
if db != nil && db.DB != nil { if db != nil && db.DB != nil {
value, err := sqlcdb.New(db.DB).GetAppSetting(context.Background(), key) var value string
err := db.QueryRow(`SELECT value FROM app_settings WHERE key = $1`, key).Scan(&value)
if err == nil { if err == nil {
return value return value
} }
@@ -89,15 +88,18 @@ func settingValue(db *database.DB, key, envKey, fallback string) string {
} }
func setSetting(db *database.DB, key, value string, secret bool) error { func setSetting(db *database.DB, key, value string, secret bool) error {
return sqlcdb.New(db.DB).UpsertAppSetting(context.Background(), sqlcdb.UpsertAppSettingParams{ _, err := db.Exec(`
Key: key, INSERT INTO app_settings (key, value, is_secret, updated_at)
Value: value, VALUES ($1, $2, $3, NOW())
IsSecret: secret, ON CONFLICT (key) DO UPDATE
}) SET value = EXCLUDED.value, is_secret = EXCLUDED.is_secret, updated_at = NOW()
`, key, value, secret)
return err
} }
func deleteSetting(db *database.DB, key string) error { func deleteSetting(db *database.DB, key string) error {
return sqlcdb.New(db.DB).DeleteAppSetting(context.Background(), key) _, err := db.Exec(`DELETE FROM app_settings WHERE key = $1`, key)
return err
} }
// signupEnabled controls whether public registration stays open after the // signupEnabled controls whether public registration stays open after the
@@ -108,12 +110,11 @@ func signupEnabled(db *database.DB) bool {
} }
func isAdminUser(db *database.DB, userID string) bool { func isAdminUser(db *database.DB, userID string) bool {
parsed, err := uuid.Parse(userID) var isAdmin bool
if err != nil { if err := db.QueryRow(`SELECT is_admin FROM users WHERE id = $1`, userID).Scan(&isAdmin); err != nil {
return false return false
} }
isAdmin, err := sqlcdb.New(db.DB).GetUserIsAdmin(context.Background(), parsed) return isAdmin
return err == nil && isAdmin
} }
func requireAdmin(c *gin.Context) (*database.DB, bool) { func requireAdmin(c *gin.Context) (*database.DB, bool) {
@@ -241,7 +242,8 @@ func handleUpdateSettings(c *gin.Context) {
// resolvedTunnelToken prefers the in-app value and falls back to the env. // resolvedTunnelToken prefers the in-app value and falls back to the env.
func resolvedTunnelToken(db *database.DB) (string, string) { func resolvedTunnelToken(db *database.DB) (string, string) {
if db != nil && db.DB != nil { if db != nil && db.DB != nil {
if value, err := sqlcdb.New(db.DB).GetAppSetting(context.Background(), settingCloudflareTunnelToken); err == nil && strings.TrimSpace(value) != "" { var value string
if err := db.QueryRow(`SELECT value FROM app_settings WHERE key = $1`, settingCloudflareTunnelToken).Scan(&value); err == nil && strings.TrimSpace(value) != "" {
return strings.TrimSpace(value), "app" return strings.TrimSpace(value), "app"
} }
} }
+2 -3
View File
@@ -5,12 +5,10 @@ package api
// lives in app_settings so it survives restarts. // lives in app_settings so it survives restarts.
import ( import (
"context"
"net/http" "net/http"
"strings" "strings"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -27,7 +25,8 @@ func setupComplete(db *database.DB, hasUsers bool) bool {
if !hasUsers { if !hasUsers {
return false return false
} }
if projects, err := sqlcdb.New(db.DB).CountProjects(context.Background()); err == nil && projects > 0 { var projects int
if err := db.QueryRow(`SELECT COUNT(*) FROM projects`).Scan(&projects); err == nil && projects > 0 {
return true return true
} }
return false return false
+46 -18
View File
@@ -86,17 +86,20 @@ type sleepCandidate struct {
} }
func sweepSleeping(ctx context.Context, db *database.DB, dc *docker.Client, engine *deployment.DeploymentEngine, apiPort int) { func sweepSleeping(ctx context.Context, db *database.DB, dc *docker.Client, engine *deployment.DeploymentEngine, apiPort int) {
sweepRows, err := sqlcdb.New(db.DB).ListSleepCandidates(ctx) rows, err := db.QueryContext(ctx,
`SELECT id, project_id, name, COALESCE(status,''), COALESCE(sleep_idle_minutes,15)
FROM services WHERE sleep_enabled AND COALESCE(status,'') IN ('running','deployed','degraded','starting')`)
if err != nil { if err != nil {
return return
} }
var candidates []sleepCandidate var candidates []sleepCandidate
for _, r := range sweepRows { for rows.Next() {
candidates = append(candidates, sleepCandidate{ var s sleepCandidate
ID: r.ID, ProjectID: r.ProjectID, Name: r.Name, if err := rows.Scan(&s.ID, &s.ProjectID, &s.Name, &s.Status, &s.IdleMinutes); err == nil {
Status: r.Status, IdleMinutes: int(r.SleepIdleMinutes), candidates = append(candidates, s)
}) }
} }
rows.Close()
q := sqlcdb.New(db.DB) q := sqlcdb.New(db.DB)
for _, svc := range candidates { for _, svc := range candidates {
@@ -184,10 +187,11 @@ func putToSleepRemote(ctx context.Context, db *database.DB, q *sqlcdb.Queries, s
tracker.mu.Lock() tracker.mu.Lock()
delete(tracker.activity, svc.ID) delete(tracker.activity, svc.ID)
tracker.mu.Unlock() tracker.mu.Unlock()
setServiceStatusNow(db, svc.ID, "sleeping") _, _ = db.Exec(`UPDATE services SET status = 'sleeping', updated_at = $1 WHERE id = $2`, time.Now(), svc.ID)
log.Printf("sleep: %s (%s) remote replicas removed after idle timeout", svc.Name, svc.ID) log.Printf("sleep: %s (%s) remote replicas removed after idle timeout", svc.Name, svc.ID)
domain, _ := q.GetServiceDomainText(ctx, svc.ID) var domain string
_ = db.QueryRow(`SELECT COALESCE(domain,'') FROM services WHERE id = $1`, svc.ID).Scan(&domain)
domains := serviceDomainNames(db, svc.ID, domain) domains := serviceDomainNames(db, svc.ID, domain)
if len(domains) > 0 { if len(domains) > 0 {
writeWakeIngress(svc.ID.String(), domains, apiPort) writeWakeIngress(svc.ID.String(), domains, apiPort)
@@ -246,10 +250,11 @@ func putToSleep(ctx context.Context, db *database.DB, dc *docker.Client, engine
tracker.mu.Lock() tracker.mu.Lock()
delete(tracker.activity, svc.ID) delete(tracker.activity, svc.ID)
tracker.mu.Unlock() tracker.mu.Unlock()
setServiceStatusNow(db, svc.ID, "sleeping") _, _ = db.Exec(`UPDATE services SET status = 'sleeping', updated_at = $1 WHERE id = $2`, time.Now(), svc.ID)
log.Printf("sleep: %s (%s) scaled to zero after idle timeout", svc.Name, svc.ID) log.Printf("sleep: %s (%s) scaled to zero after idle timeout", svc.Name, svc.ID)
domain, _ := sqlcdb.New(db.DB).GetServiceDomainText(ctx, svc.ID) var domain string
_ = db.QueryRow(`SELECT COALESCE(domain,'') FROM services WHERE id = $1`, svc.ID).Scan(&domain)
domains := serviceDomainNames(db, svc.ID, domain) domains := serviceDomainNames(db, svc.ID, domain)
if len(domains) == 0 { if len(domains) == 0 {
return return
@@ -439,9 +444,7 @@ func pruneWakeContainers(ctx context.Context, db *database.DB, dc *docker.Client
} }
} }
var status string var status string
if sid, err := uuid.Parse(svcID); err == nil { _ = db.QueryRow(`SELECT COALESCE(status,'') FROM services WHERE id = $1`, svcID).Scan(&status)
status, _ = sqlcdb.New(db.DB).GetServiceStatusText(ctx, sid)
}
if live > 0 || status == "stopped" || status == "" { if live > 0 || status == "stopped" || status == "" {
_ = dc.RemoveContainer(ctx, c.ID, true) _ = dc.RemoveContainer(ctx, c.ID, true)
} }
@@ -488,11 +491,34 @@ func triggerServiceWake(c *gin.Context) (int, gin.H) {
if err != nil { if err != nil {
return http.StatusBadRequest, gin.H{"error": "invalid service id"} return http.StatusBadRequest, gin.H{"error": "invalid service id"}
} }
row, err := sqlcdb.New(db.DB).GetServiceRuntimeWithOwner(context.Background(), id) var service Service
err = db.QueryRow(
`SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, ''), COALESCE(s.status, ''),
COALESCE(s.image, s.image_name, ''), COALESCE(s.command, s.start_command, ''),
COALESCE(s.environment, ''), COALESCE(s.git_repo, s.source_url, ''),
COALESCE(s.git_branch, ''), COALESCE(s.build_path, ''),
COALESCE(s.cpu, ''), COALESCE(s.memory, ''),
COALESCE(s.replicas, 1), COALESCE(s.port, 0),
COALESCE(s.domain, ''), COALESCE(s.healthcheck_path, ''),
COALESCE(s.restart_policy, 'unless-stopped'),
COALESCE(s.builder, 'auto'), COALESCE(s.cpu_reserve, ''),
COALESCE(s.memory_reserve, ''), COALESCE(s.static_build_cmd, ''),
COALESCE(s.static_dir, ''),
s.created_at, s.updated_at
FROM services s WHERE s.id = $1`, id,
).Scan(
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
&service.Replicas, &service.Port, &service.Domain, &service.HealthCheckPath,
&service.RestartPolicy, &service.Builder, &service.CPUReserve,
&service.MemoryReserve, &service.StaticBuildCmd, &service.StaticDir,
&service.CreatedAt, &service.UpdatedAt,
)
if err != nil { if err != nil {
return http.StatusNotFound, gin.H{"error": "service not found"} return http.StatusNotFound, gin.H{"error": "service not found"}
} }
service := serviceFromRuntimeRow(row)
if service.Status == "running" || service.Status == "deployed" { if service.Status == "running" || service.Status == "deployed" {
return http.StatusOK, gin.H{"status": service.Status} return http.StatusOK, gin.H{"status": service.Status}
} }
@@ -502,13 +528,13 @@ func triggerServiceWake(c *gin.Context) (int, gin.H) {
spec, err := serviceRuntimeSpec(db, service) spec, err := serviceRuntimeSpec(db, service)
if err != nil { if err != nil {
log.Printf("wake: runtime spec for %s failed: %v", service.ID, err) log.Printf("wake: runtime spec for %s failed: %v", service.ID, err)
setServiceStatusNow(db, service.ID, "failed") _, _ = db.Exec(`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
return return
} }
state, err := engine.ReconcileService(ctx, spec) state, err := engine.ReconcileService(ctx, spec)
if err != nil { if err != nil {
log.Printf("wake: reconcile for %s failed: %v", service.ID, err) log.Printf("wake: reconcile for %s failed: %v", service.ID, err)
setServiceStatusNow(db, service.ID, "failed") _, _ = db.Exec(`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
return return
} }
status := "running" status := "running"
@@ -516,7 +542,7 @@ func triggerServiceWake(c *gin.Context) (int, gin.H) {
status = state.Status status = state.Status
persistPublishedPort(db, service.ID, state.Ports) persistPublishedPort(db, service.ID, state.Ports)
} }
setServiceStatusNow(db, service.ID, status) _, _ = db.Exec(`UPDATE services SET status = $1, updated_at = $2 WHERE id = $3`, status, time.Now(), service.ID)
}() }()
return http.StatusAccepted, gin.H{"status": "waking"} return http.StatusAccepted, gin.H{"status": "waking"}
} }
@@ -537,6 +563,8 @@ func handleServiceSleep(c *gin.Context) {
if !ok { if !ok {
return return
} }
var port int
_ = db.QueryRow(`SELECT COALESCE(port,0) FROM services WHERE id = $1`, service.ID).Scan(&port)
svc := sleepCandidate{ svc := sleepCandidate{
ID: service.ID, ID: service.ID,
ProjectID: service.ProjectID, ProjectID: service.ProjectID,
+18 -33
View File
@@ -640,20 +640,19 @@ func deployTemplateGraph(
createdServiceIDs := []uuid.UUID{} createdServiceIDs := []uuid.UUID{}
createdDatabaseIDs := []string{} createdDatabaseIDs := []string{}
cleanup := func() { cleanup := func() {
q := sqlcdb.New(db.DB)
for _, sid := range createdServiceIDs { for _, sid := range createdServiceIDs {
if engine != nil { if engine != nil {
removeServiceRuntime(context.Background(), db, engine, sid) removeServiceRuntime(context.Background(), db, engine, sid)
} }
_ = q.DeleteEnvVarsForService(context.Background(), sid) _, _ = db.Exec(`DELETE FROM environment_variables WHERE service_id = $1`, sid)
_ = q.DeleteServiceByID(context.Background(), sid) _, _ = db.Exec(`DELETE FROM services WHERE id = $1`, sid)
} }
for _, did := range createdDatabaseIDs { for _, did := range createdDatabaseIDs {
if dbHandler != nil && dbHandler.dockerClient != nil { if dbHandler != nil && dbHandler.dockerClient != nil {
_ = dbHandler.dockerClient.RemoveContainer(context.Background(), managedDatabaseContainerName(did), true) _ = dbHandler.dockerClient.RemoveContainer(context.Background(), managedDatabaseContainerName(did), true)
_ = dbHandler.dockerClient.RemoveVolume(context.Background(), managedDatabaseVolumeName(did), true) _ = dbHandler.dockerClient.RemoveVolume(context.Background(), managedDatabaseVolumeName(did), true)
} }
_ = q.DeleteDatabaseServiceByID(context.Background(), did) _, _ = db.Exec(`DELETE FROM database_services WHERE id = $1`, did)
} }
} }
@@ -773,19 +772,14 @@ func deployTemplateGraph(
if builder == "" { if builder == "" {
builder = "auto" builder = "auto"
} }
var volumesRaw json.RawMessage if _, err = tx.ExecContext(ctx,
if s, ok := volumesJSON.(string); ok { `UPDATE services SET port = $1, healthcheck_path = $2, volumes = COALESCE($3::jsonb, '[]'::jsonb),
volumesRaw = json.RawMessage(s) git_repo = NULLIF($4, ''), git_branch = NULLIF($5, ''), builder = $6
} WHERE id = $7`,
if err = sqlcdb.New(tx).UpdateServiceDeployMeta(ctx, sqlcdb.UpdateServiceDeployMetaParams{ m.spec.Port, strings.TrimSpace(m.spec.HealthCheck), volumesJSON,
Port: int32(m.spec.Port), strings.TrimSpace(m.spec.Repo), strings.TrimSpace(m.spec.Branch), builder,
HealthcheckPath: strings.TrimSpace(m.spec.HealthCheck), serviceID,
Column3: volumesRaw, ); err != nil {
Column4: strings.TrimSpace(m.spec.Repo),
Column5: strings.TrimSpace(m.spec.Branch),
Builder: builder,
ID: serviceID,
}); err != nil {
tx.Rollback() tx.Rollback()
cleanup() cleanup()
return nil, &gin.H{"error": "Failed to configure service"} return nil, &gin.H{"error": "Failed to configure service"}
@@ -867,24 +861,15 @@ func queueTemplateDeployment(c *gin.Context, db *database.DB, engine *deployment
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
q := sqlcdb.New(db.DB) if _, err := db.Exec(
if err := q.InsertDeployment(context.Background(), sqlcdb.InsertDeploymentParams{ `INSERT INTO deployments
ID: dep.ID, (id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
ServiceID: dep.ServiceID, VALUES ($1, $2, $3, NULL, $4, '', '', $5, $6)`,
Version: fmt.Sprintf("v%d", now.Unix()), dep.ID, dep.ServiceID, fmt.Sprintf("v%d", now.Unix()), dep.Status, dep.CreatedAt, dep.UpdatedAt,
Status: sql.NullString{String: dep.Status, Valid: true}, ); err != nil {
ImageName: sql.NullString{String: "", Valid: true},
ImageTag: sql.NullString{String: "", Valid: true},
CreatedAt: sql.NullTime{Time: dep.CreatedAt, Valid: true},
UpdatedAt: sql.NullTime{Time: dep.UpdatedAt, Valid: true},
}); err != nil {
return uuid.Nil return uuid.Nil
} }
_ = q.SetServiceStatus(context.Background(), sqlcdb.SetServiceStatusParams{ _, _ = db.Exec(`UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`, time.Now(), service.ID)
Status: sql.NullString{String: "queued", Valid: true},
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: service.ID,
})
getDeployQueue(c).Enqueue(service.ID, deployqueue.Job{ getDeployQueue(c).Enqueue(service.ID, deployqueue.Job{
DeploymentID: dep.ID, DeploymentID: dep.ID,
+6 -5
View File
@@ -411,11 +411,12 @@ func handleCreateFromTemplate(c *gin.Context) {
return return
} }
if err = sqlcdb.New(tx).UpdateServiceRuntimeMeta(ctx, sqlcdb.UpdateServiceRuntimeMetaParams{ // sqlc's schema predates the runtime columns; port and healthcheck_path are
Port: int32(config.Port), // written via raw SQL like every other service mutation path.
HealthcheckPath: strings.TrimSpace(config.HealthCheck), if _, err = tx.ExecContext(ctx,
ID: serviceID, `UPDATE services SET port = $1, healthcheck_path = $2 WHERE id = $3`,
}); err != nil { config.Port, strings.TrimSpace(config.HealthCheck), serviceID,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service from template"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service from template"})
return return
} }
+9 -16
View File
@@ -1,14 +1,11 @@
package api package api
import ( import (
"database/sql" "containr/internal/database"
"containr/internal/secrets"
"net/http" "net/http"
"time" "time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/secrets"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -150,8 +147,7 @@ func handleUpdateVariables(c *gin.Context) {
} }
defer tx.Rollback() defer tx.Rollback()
txq := sqlcdb.New(tx) _, err = tx.Exec("DELETE FROM environment_variables WHERE service_id = $1", serviceID)
err = txq.DeleteEnvVarsForService(c.Request.Context(), serviceID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"})
return return
@@ -168,15 +164,12 @@ func handleUpdateVariables(c *gin.Context) {
if v.IsSecret { if v.IsSecret {
value = secrets.Encrypt(value) value = secrets.Encrypt(value)
} }
err = txq.InsertEnvVar(c.Request.Context(), sqlcdb.InsertEnvVarParams{ varID := uuid.New()
ID: uuid.New(), _, err = tx.Exec(
ServiceID: serviceID, `INSERT INTO environment_variables (id, service_id, key, value, is_secret, created_at, updated_at)
Key: v.Key, VALUES ($1, $2, $3, $4, $5, $6, $7)`,
Value: value, varID, serviceID, v.Key, value, v.IsSecret, now, now,
IsSecret: sql.NullBool{Bool: v.IsSecret, Valid: true}, )
CreatedAt: sql.NullTime{Time: now, Valid: true},
UpdatedAt: sql.NullTime{Time: now, Valid: true},
})
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key})
return return
@@ -1,636 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: apwhy.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const countAPKeys = `-- name: CountAPKeys :one
SELECT COUNT(*) FROM api_keys
`
func (q *Queries) CountAPKeys(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, countAPKeys)
var count int64
err := row.Scan(&count)
return count, err
}
const countAPServices = `-- name: CountAPServices :one
SELECT COUNT(*) FROM api_services
`
func (q *Queries) CountAPServices(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, countAPServices)
var count int64
err := row.Scan(&count)
return count, err
}
const getAPServiceBySlug = `-- name: GetAPServiceBySlug :one
SELECT id, upstream_url, route_prefix, upstream_auth_header,
upstream_auth_value, enabled, rpm_limit, monthly_quota,
request_timeout_ms
FROM api_services WHERE slug = $1
`
type GetAPServiceBySlugRow struct {
ID uuid.UUID `json:"id"`
UpstreamUrl string `json:"upstream_url"`
RoutePrefix string `json:"route_prefix"`
UpstreamAuthHeader sql.NullString `json:"upstream_auth_header"`
UpstreamAuthValue sql.NullString `json:"upstream_auth_value"`
Enabled int32 `json:"enabled"`
RpmLimit sql.NullInt32 `json:"rpm_limit"`
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
RequestTimeoutMs sql.NullInt32 `json:"request_timeout_ms"`
}
func (q *Queries) GetAPServiceBySlug(ctx context.Context, slug string) (GetAPServiceBySlugRow, error) {
row := q.db.QueryRowContext(ctx, getAPServiceBySlug, slug)
var i GetAPServiceBySlugRow
err := row.Scan(
&i.ID,
&i.UpstreamUrl,
&i.RoutePrefix,
&i.UpstreamAuthHeader,
&i.UpstreamAuthValue,
&i.Enabled,
&i.RpmLimit,
&i.MonthlyQuota,
&i.RequestTimeoutMs,
)
return i, err
}
const getAPServiceForValidate = `-- name: GetAPServiceForValidate :one
SELECT upstream_url, health_path, upstream_auth_header, upstream_auth_value, request_timeout_ms
FROM api_services WHERE id = $1
`
type GetAPServiceForValidateRow struct {
UpstreamUrl string `json:"upstream_url"`
HealthPath string `json:"health_path"`
UpstreamAuthHeader sql.NullString `json:"upstream_auth_header"`
UpstreamAuthValue sql.NullString `json:"upstream_auth_value"`
RequestTimeoutMs sql.NullInt32 `json:"request_timeout_ms"`
}
func (q *Queries) GetAPServiceForValidate(ctx context.Context, id uuid.UUID) (GetAPServiceForValidateRow, error) {
row := q.db.QueryRowContext(ctx, getAPServiceForValidate, id)
var i GetAPServiceForValidateRow
err := row.Scan(
&i.UpstreamUrl,
&i.HealthPath,
&i.UpstreamAuthHeader,
&i.UpstreamAuthValue,
&i.RequestTimeoutMs,
)
return i, err
}
const insertAPKey = `-- name: InsertAPKey :one
INSERT INTO api_keys (
name, key_hash, key_prefix, plan, allowed_service_ids,
rpm_limit, monthly_quota, created_at, updated_at
) VALUES ($1, $2, $3, $4, '[]', $5, $6, NOW(), NOW())
RETURNING id
`
type InsertAPKeyParams struct {
Name string `json:"name"`
KeyHash string `json:"key_hash"`
KeyPrefix string `json:"key_prefix"`
Plan string `json:"plan"`
RpmLimit sql.NullInt32 `json:"rpm_limit"`
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
}
func (q *Queries) InsertAPKey(ctx context.Context, arg InsertAPKeyParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, insertAPKey,
arg.Name,
arg.KeyHash,
arg.KeyPrefix,
arg.Plan,
arg.RpmLimit,
arg.MonthlyQuota,
)
var id uuid.UUID
err := row.Scan(&id)
return id, err
}
const insertAPService = `-- name: InsertAPService :one
INSERT INTO api_services (
name, slug, upstream_url, route_prefix, health_path,
rpm_limit, monthly_quota, created_at, updated_at
) VALUES ($1, $2, $3, $4, '/health', $5, $6, NOW(), NOW())
RETURNING id
`
type InsertAPServiceParams struct {
Name string `json:"name"`
Slug string `json:"slug"`
UpstreamUrl string `json:"upstream_url"`
RoutePrefix string `json:"route_prefix"`
RpmLimit sql.NullInt32 `json:"rpm_limit"`
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
}
func (q *Queries) InsertAPService(ctx context.Context, arg InsertAPServiceParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, insertAPService,
arg.Name,
arg.Slug,
arg.UpstreamUrl,
arg.RoutePrefix,
arg.RpmLimit,
arg.MonthlyQuota,
)
var id uuid.UUID
err := row.Scan(&id)
return id, err
}
const insertGatewayIncident = `-- name: InsertGatewayIncident :exec
INSERT INTO incident_events (service_id, api_key_id, code, message, severity, http_status, occurred_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())
`
type InsertGatewayIncidentParams struct {
ServiceID uuid.NullUUID `json:"service_id"`
ApiKeyID uuid.NullUUID `json:"api_key_id"`
Code string `json:"code"`
Message string `json:"message"`
Severity string `json:"severity"`
HttpStatus sql.NullInt32 `json:"http_status"`
}
func (q *Queries) InsertGatewayIncident(ctx context.Context, arg InsertGatewayIncidentParams) error {
_, err := q.db.ExecContext(ctx, insertGatewayIncident,
arg.ServiceID,
arg.ApiKeyID,
arg.Code,
arg.Message,
arg.Severity,
arg.HttpStatus,
)
return err
}
const insertMetricPoint = `-- name: InsertMetricPoint :exec
INSERT INTO metrics_timeseries (metric, value, labels_json, occurred_at)
VALUES ('gateway_request', 1, $1, NOW())
`
func (q *Queries) InsertMetricPoint(ctx context.Context, labelsJson sql.NullString) error {
_, err := q.db.ExecContext(ctx, insertMetricPoint, labelsJson)
return err
}
const insertServiceIncident = `-- name: InsertServiceIncident :exec
INSERT INTO incident_events (service_id, code, message, severity, http_status, occurred_at)
VALUES ($1, 'SERVICE_VALIDATION_FAILED', $2, 'medium', $3, NOW())
`
type InsertServiceIncidentParams struct {
ServiceID uuid.NullUUID `json:"service_id"`
Message string `json:"message"`
HttpStatus sql.NullInt32 `json:"http_status"`
}
func (q *Queries) InsertServiceIncident(ctx context.Context, arg InsertServiceIncidentParams) error {
_, err := q.db.ExecContext(ctx, insertServiceIncident, arg.ServiceID, arg.Message, arg.HttpStatus)
return err
}
const listAPKeys = `-- name: ListAPKeys :many
SELECT id, name, key_prefix, plan, enabled, rpm_limit, monthly_quota,
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
to_char(updated_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS updated_at
FROM api_keys
ORDER BY created_at DESC
`
type ListAPKeysRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
KeyPrefix string `json:"key_prefix"`
Plan string `json:"plan"`
Enabled int32 `json:"enabled"`
RpmLimit sql.NullInt32 `json:"rpm_limit"`
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (q *Queries) ListAPKeys(ctx context.Context) ([]ListAPKeysRow, error) {
rows, err := q.db.QueryContext(ctx, listAPKeys)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListAPKeysRow{}
for rows.Next() {
var i ListAPKeysRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.KeyPrefix,
&i.Plan,
&i.Enabled,
&i.RpmLimit,
&i.MonthlyQuota,
&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 listAPKeysByPrefix = `-- name: ListAPKeysByPrefix :many
SELECT id, key_hash, enabled, rpm_limit, monthly_quota, allowed_service_ids
FROM api_keys WHERE key_prefix = $1
`
type ListAPKeysByPrefixRow struct {
ID uuid.UUID `json:"id"`
KeyHash string `json:"key_hash"`
Enabled int32 `json:"enabled"`
RpmLimit sql.NullInt32 `json:"rpm_limit"`
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
AllowedServiceIds string `json:"allowed_service_ids"`
}
func (q *Queries) ListAPKeysByPrefix(ctx context.Context, keyPrefix string) ([]ListAPKeysByPrefixRow, error) {
rows, err := q.db.QueryContext(ctx, listAPKeysByPrefix, keyPrefix)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListAPKeysByPrefixRow{}
for rows.Next() {
var i ListAPKeysByPrefixRow
if err := rows.Scan(
&i.ID,
&i.KeyHash,
&i.Enabled,
&i.RpmLimit,
&i.MonthlyQuota,
&i.AllowedServiceIds,
); 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 listAPServices = `-- name: ListAPServices :many
SELECT id, name, slug, upstream_url, route_prefix, enabled,
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
to_char(updated_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS updated_at
FROM api_services
ORDER BY created_at DESC
`
type ListAPServicesRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
UpstreamUrl string `json:"upstream_url"`
RoutePrefix string `json:"route_prefix"`
Enabled int32 `json:"enabled"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (q *Queries) ListAPServices(ctx context.Context) ([]ListAPServicesRow, error) {
rows, err := q.db.QueryContext(ctx, listAPServices)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListAPServicesRow{}
for rows.Next() {
var i ListAPServicesRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Slug,
&i.UpstreamUrl,
&i.RoutePrefix,
&i.Enabled,
&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 listClientEventPaths = `-- name: ListClientEventPaths :many
SELECT COALESCE((labels_json::jsonb ->> 'path'), 'unknown') AS path, COUNT(*) AS total
FROM metrics_timeseries
WHERE metric = 'client_event' AND occurred_at >= NOW() - INTERVAL '7 days'
GROUP BY COALESCE((labels_json::jsonb ->> 'path'), 'unknown')
ORDER BY total DESC, path ASC
LIMIT 20
`
type ListClientEventPathsRow struct {
Path interface{} `json:"path"`
Total int64 `json:"total"`
}
func (q *Queries) ListClientEventPaths(ctx context.Context) ([]ListClientEventPathsRow, error) {
rows, err := q.db.QueryContext(ctx, listClientEventPaths)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListClientEventPathsRow{}
for rows.Next() {
var i ListClientEventPathsRow
if err := rows.Scan(&i.Path, &i.Total); 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 listIncidentStatusCodes = `-- name: ListIncidentStatusCodes :many
SELECT COALESCE(http_status, 0) AS status_code, COALESCE(SUM(count), 0) AS total
FROM incident_events
WHERE occurred_at >= NOW() - INTERVAL '7 days'
GROUP BY COALESCE(http_status, 0)
ORDER BY total DESC, status_code ASC
`
type ListIncidentStatusCodesRow struct {
StatusCode int32 `json:"status_code"`
Total interface{} `json:"total"`
}
func (q *Queries) ListIncidentStatusCodes(ctx context.Context) ([]ListIncidentStatusCodesRow, error) {
rows, err := q.db.QueryContext(ctx, listIncidentStatusCodes)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListIncidentStatusCodesRow{}
for rows.Next() {
var i ListIncidentStatusCodesRow
if err := rows.Scan(&i.StatusCode, &i.Total); 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 listRequestsByDay = `-- name: ListRequestsByDay :many
SELECT TO_CHAR(DATE_TRUNC('day', occurred_at), 'YYYY-MM-DD') AS day_bucket, COUNT(*) AS total
FROM metrics_timeseries
WHERE metric = 'request_total' AND occurred_at >= NOW() - INTERVAL '7 days'
GROUP BY DATE_TRUNC('day', occurred_at)
ORDER BY DATE_TRUNC('day', occurred_at) ASC
`
type ListRequestsByDayRow struct {
DayBucket string `json:"day_bucket"`
Total int64 `json:"total"`
}
func (q *Queries) ListRequestsByDay(ctx context.Context) ([]ListRequestsByDayRow, error) {
rows, err := q.db.QueryContext(ctx, listRequestsByDay)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListRequestsByDayRow{}
for rows.Next() {
var i ListRequestsByDayRow
if err := rows.Scan(&i.DayBucket, &i.Total); 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 listTopAPServices = `-- name: ListTopAPServices :many
SELECT s.id, s.name, COALESCE(SUM(u.request_count), 0) AS total_requests
FROM api_services s
LEFT JOIN usage_counters u ON u.service_id = s.id
GROUP BY s.id, s.name
ORDER BY total_requests DESC, s.name ASC
LIMIT 10
`
type ListTopAPServicesRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
TotalRequests interface{} `json:"total_requests"`
}
func (q *Queries) ListTopAPServices(ctx context.Context) ([]ListTopAPServicesRow, error) {
rows, err := q.db.QueryContext(ctx, listTopAPServices)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListTopAPServicesRow{}
for rows.Next() {
var i ListTopAPServicesRow
if err := rows.Scan(&i.ID, &i.Name, &i.TotalRequests); 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 setAPKeyEnabled = `-- name: SetAPKeyEnabled :exec
UPDATE api_keys SET enabled = $1, updated_at = NOW() WHERE id = $2
`
type SetAPKeyEnabledParams struct {
Enabled int32 `json:"enabled"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetAPKeyEnabled(ctx context.Context, arg SetAPKeyEnabledParams) error {
_, err := q.db.ExecContext(ctx, setAPKeyEnabled, arg.Enabled, arg.ID)
return err
}
const setAPServiceEnabled = `-- name: SetAPServiceEnabled :exec
UPDATE api_services SET enabled = $1, updated_at = NOW() WHERE id = $2
`
type SetAPServiceEnabledParams struct {
Enabled int32 `json:"enabled"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetAPServiceEnabled(ctx context.Context, arg SetAPServiceEnabledParams) error {
_, err := q.db.ExecContext(ctx, setAPServiceEnabled, arg.Enabled, arg.ID)
return err
}
const setAPServiceValidation = `-- name: SetAPServiceValidation :exec
UPDATE api_services
SET last_validation_at = NOW(), last_validation_status = $1, last_validation_message = $2, updated_at = NOW()
WHERE id = $3
`
type SetAPServiceValidationParams struct {
LastValidationStatus sql.NullString `json:"last_validation_status"`
LastValidationMessage sql.NullString `json:"last_validation_message"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetAPServiceValidation(ctx context.Context, arg SetAPServiceValidationParams) error {
_, err := q.db.ExecContext(ctx, setAPServiceValidation, arg.LastValidationStatus, arg.LastValidationMessage, arg.ID)
return err
}
const sumRequestMetricSince = `-- name: SumRequestMetricSince :one
SELECT COALESCE(SUM(value), 0)::int
FROM metrics_timeseries
WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC($1::text, NOW())
`
func (q *Queries) SumRequestMetricSince(ctx context.Context, dollar_1 string) (int32, error) {
row := q.db.QueryRowContext(ctx, sumRequestMetricSince, dollar_1)
var column_1 int32
err := row.Scan(&column_1)
return column_1, err
}
const sumUsageByKey = `-- name: SumUsageByKey :one
SELECT COALESCE(SUM(request_count), 0)::bigint FROM usage_counters
WHERE api_key_id = $1 AND period_month = $2
`
type SumUsageByKeyParams struct {
ApiKeyID uuid.UUID `json:"api_key_id"`
PeriodMonth string `json:"period_month"`
}
func (q *Queries) SumUsageByKey(ctx context.Context, arg SumUsageByKeyParams) (int64, error) {
row := q.db.QueryRowContext(ctx, sumUsageByKey, arg.ApiKeyID, arg.PeriodMonth)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const sumUsageByService = `-- name: SumUsageByService :one
SELECT COALESCE(SUM(request_count), 0)::bigint FROM usage_counters
WHERE service_id = $1 AND period_month = $2
`
type SumUsageByServiceParams struct {
ServiceID uuid.UUID `json:"service_id"`
PeriodMonth string `json:"period_month"`
}
func (q *Queries) SumUsageByService(ctx context.Context, arg SumUsageByServiceParams) (int64, error) {
row := q.db.QueryRowContext(ctx, sumUsageByService, arg.ServiceID, arg.PeriodMonth)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const sumUsageCounters = `-- name: SumUsageCounters :one
SELECT COALESCE(SUM(request_count), 0) FROM usage_counters
`
func (q *Queries) SumUsageCounters(ctx context.Context) (interface{}, error) {
row := q.db.QueryRowContext(ctx, sumUsageCounters)
var coalesce interface{}
err := row.Scan(&coalesce)
return coalesce, err
}
const touchAPKeyLastUsed = `-- name: TouchAPKeyLastUsed :exec
UPDATE api_keys SET last_used_at = NOW() WHERE id = $1
`
func (q *Queries) TouchAPKeyLastUsed(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, touchAPKeyLastUsed, id)
return err
}
const upsertUsageCounter = `-- name: UpsertUsageCounter :exec
INSERT INTO usage_counters (api_key_id, service_id, period_month, request_count, updated_at)
VALUES ($1, $2, $3, 1, NOW())
ON CONFLICT (api_key_id, service_id, period_month)
DO UPDATE SET request_count = usage_counters.request_count + 1, updated_at = NOW()
`
type UpsertUsageCounterParams struct {
ApiKeyID uuid.UUID `json:"api_key_id"`
ServiceID uuid.UUID `json:"service_id"`
PeriodMonth string `json:"period_month"`
}
func (q *Queries) UpsertUsageCounter(ctx context.Context, arg UpsertUsageCounterParams) error {
_, err := q.db.ExecContext(ctx, upsertUsageCounter, arg.ApiKeyID, arg.ServiceID, arg.PeriodMonth)
return err
}
@@ -1,218 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: audit.sql
package sqlcdb
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
)
const insertAuditLog = `-- name: InsertAuditLog :exec
INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, severity, category, label, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`
type InsertAuditLogParams struct {
ID uuid.UUID `json:"id"`
UserID uuid.NullUUID `json:"user_id"`
Resource string `json:"resource"`
ResourceID uuid.NullUUID `json:"resource_id"`
Action string `json:"action"`
Details pqtype.NullRawMessage `json:"details"`
Severity string `json:"severity"`
Category string `json:"category"`
Label string `json:"label"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error {
_, err := q.db.ExecContext(ctx, insertAuditLog,
arg.ID,
arg.UserID,
arg.Resource,
arg.ResourceID,
arg.Action,
arg.Details,
arg.Severity,
arg.Category,
arg.Label,
arg.CreatedAt,
)
return err
}
const insertAuditLogWithRequest = `-- name: InsertAuditLogWithRequest :exec
INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, ip_address, user_agent, severity, category, label, created_at)
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::inet, $8, $9, $10, $11, $12)
`
type InsertAuditLogWithRequestParams struct {
ID uuid.UUID `json:"id"`
UserID uuid.NullUUID `json:"user_id"`
Resource string `json:"resource"`
ResourceID uuid.NullUUID `json:"resource_id"`
Action string `json:"action"`
Column6 json.RawMessage `json:"column_6"`
Column7 pqtype.Inet `json:"column_7"`
UserAgent sql.NullString `json:"user_agent"`
Severity string `json:"severity"`
Category string `json:"category"`
Label string `json:"label"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) InsertAuditLogWithRequest(ctx context.Context, arg InsertAuditLogWithRequestParams) error {
_, err := q.db.ExecContext(ctx, insertAuditLogWithRequest,
arg.ID,
arg.UserID,
arg.Resource,
arg.ResourceID,
arg.Action,
arg.Column6,
arg.Column7,
arg.UserAgent,
arg.Severity,
arg.Category,
arg.Label,
arg.CreatedAt,
)
return err
}
const listAuditBackfillBatch = `-- name: ListAuditBackfillBatch :many
SELECT id::text, resource, action FROM audit_logs
WHERE label = '' AND id > $1::uuid ORDER BY id LIMIT 500
`
type ListAuditBackfillBatchRow struct {
ID string `json:"id"`
Resource string `json:"resource"`
Action string `json:"action"`
}
func (q *Queries) ListAuditBackfillBatch(ctx context.Context, dollar_1 uuid.UUID) ([]ListAuditBackfillBatchRow, error) {
rows, err := q.db.QueryContext(ctx, listAuditBackfillBatch, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListAuditBackfillBatchRow{}
for rows.Next() {
var i ListAuditBackfillBatchRow
if err := rows.Scan(&i.ID, &i.Resource, &i.Action); 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 listResourceAuditLogs = `-- name: ListResourceAuditLogs :many
SELECT id, COALESCE(user_id::text, '')::text AS user_id, resource,
COALESCE(resource_id::text, '')::text AS resource_id, action,
COALESCE(details::text, '{}')::text AS details,
COALESCE(ip_address::text, '')::text AS ip_address,
COALESCE(user_agent, '') AS user_agent, created_at
FROM audit_logs
WHERE user_id::text = $1 AND resource = $2 AND resource_id::text = $3
ORDER BY created_at DESC
LIMIT 100
`
type ListResourceAuditLogsParams struct {
UserID uuid.NullUUID `json:"user_id"`
Resource string `json:"resource"`
ResourceID uuid.NullUUID `json:"resource_id"`
}
type ListResourceAuditLogsRow struct {
ID uuid.UUID `json:"id"`
UserID string `json:"user_id"`
Resource string `json:"resource"`
ResourceID string `json:"resource_id"`
Action string `json:"action"`
Details string `json:"details"`
IpAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) ListResourceAuditLogs(ctx context.Context, arg ListResourceAuditLogsParams) ([]ListResourceAuditLogsRow, error) {
rows, err := q.db.QueryContext(ctx, listResourceAuditLogs, arg.UserID, arg.Resource, arg.ResourceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListResourceAuditLogsRow{}
for rows.Next() {
var i ListResourceAuditLogsRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Resource,
&i.ResourceID,
&i.Action,
&i.Details,
&i.IpAddress,
&i.UserAgent,
&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 pruneAuditLogs = `-- name: PruneAuditLogs :execrows
DELETE FROM audit_logs WHERE created_at < NOW() - ($1::int * INTERVAL '1 day')
`
func (q *Queries) PruneAuditLogs(ctx context.Context, dollar_1 int32) (int64, error) {
result, err := q.db.ExecContext(ctx, pruneAuditLogs, dollar_1)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const setAuditLogMetadata = `-- name: SetAuditLogMetadata :exec
UPDATE audit_logs SET severity = $1, category = $2, label = $3 WHERE id = $4::uuid
`
type SetAuditLogMetadataParams struct {
Severity string `json:"severity"`
Category string `json:"category"`
Label string `json:"label"`
Column4 uuid.UUID `json:"column_4"`
}
func (q *Queries) SetAuditLogMetadata(ctx context.Context, arg SetAuditLogMetadataParams) error {
_, err := q.db.ExecContext(ctx, setAuditLogMetadata,
arg.Severity,
arg.Category,
arg.Label,
arg.Column4,
)
return err
}
@@ -1,335 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: auth.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const countUsers = `-- name: CountUsers :one
SELECT COUNT(*) FROM users
`
func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, countUsers)
var count int64
err := row.Scan(&count)
return count, err
}
const countUsersByEmail = `-- name: CountUsersByEmail :one
SELECT COUNT(*) FROM users WHERE email = $1
`
func (q *Queries) CountUsersByEmail(ctx context.Context, email string) (int64, error) {
row := q.db.QueryRowContext(ctx, countUsersByEmail, email)
var count int64
err := row.Scan(&count)
return count, err
}
const getAdminUserInfo = `-- name: GetAdminUserInfo :one
SELECT email, COALESCE(name, '') AS name, is_admin
FROM users WHERE id = $1
`
type GetAdminUserInfoRow struct {
Email string `json:"email"`
Name string `json:"name"`
IsAdmin bool `json:"is_admin"`
}
func (q *Queries) GetAdminUserInfo(ctx context.Context, id uuid.UUID) (GetAdminUserInfoRow, error) {
row := q.db.QueryRowContext(ctx, getAdminUserInfo, id)
var i GetAdminUserInfoRow
err := row.Scan(&i.Email, &i.Name, &i.IsAdmin)
return i, err
}
const getUserByEmailForAuth = `-- name: GetUserByEmailForAuth :one
SELECT id, email, password_hash, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE email = $1
`
type GetUserByEmailForAuthRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) GetUserByEmailForAuth(ctx context.Context, email string) (GetUserByEmailForAuthRow, error) {
row := q.db.QueryRowContext(ctx, getUserByEmailForAuth, email)
var i GetUserByEmailForAuthRow
err := row.Scan(
&i.ID,
&i.Email,
&i.PasswordHash,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE id = $1
`
type GetUserByIDRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error) {
row := q.db.QueryRowContext(ctx, getUserByID, id)
var i GetUserByIDRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const insertUser = `-- name: InsertUser :one
INSERT INTO users (email, password_hash, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
`
type InsertUserParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
}
type InsertUserRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (InsertUserRow, error) {
row := q.db.QueryRowContext(ctx, insertUser, arg.Email, arg.PasswordHash, arg.Name)
var i InsertUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const insertUserAdmin = `-- name: InsertUserAdmin :one
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, $4)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
`
type InsertUserAdminParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
IsAdmin bool `json:"is_admin"`
}
type InsertUserAdminRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams) (InsertUserAdminRow, error) {
row := q.db.QueryRowContext(ctx, insertUserAdmin,
arg.Email,
arg.PasswordHash,
arg.Name,
arg.IsAdmin,
)
var i InsertUserAdminRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const listAdminUsers = `-- name: ListAdminUsers :many
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin,
COALESCE(to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"'), '')::text AS created_at
FROM users
ORDER BY created_at ASC
`
type ListAdminUsersRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt string `json:"created_at"`
}
func (q *Queries) ListAdminUsers(ctx context.Context) ([]ListAdminUsersRow, error) {
rows, err := q.db.QueryContext(ctx, listAdminUsers)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListAdminUsersRow{}
for rows.Next() {
var i ListAdminUsersRow
if err := rows.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&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 setUserAdmin = `-- name: SetUserAdmin :execrows
UPDATE users SET is_admin = $1, updated_at = NOW() WHERE id = $2
`
type SetUserAdminParams struct {
IsAdmin bool `json:"is_admin"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetUserAdmin(ctx context.Context, arg SetUserAdminParams) (int64, error) {
result, err := q.db.ExecContext(ctx, setUserAdmin, arg.IsAdmin, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const updateUserProfile = `-- name: UpdateUserProfile :exec
UPDATE users
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
WHERE id = $3
`
type UpdateUserProfileParams struct {
Column1 string `json:"column_1"`
Column2 string `json:"column_2"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error {
_, err := q.db.ExecContext(ctx, updateUserProfile, arg.Column1, arg.Column2, arg.ID)
return err
}
const upsertAdminUser = `-- name: UpsertAdminUser :one
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, true)
ON CONFLICT (email) DO UPDATE
SET is_admin = true, updated_at = NOW()
RETURNING id
`
type UpsertAdminUserParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
}
func (q *Queries) UpsertAdminUser(ctx context.Context, arg UpsertAdminUserParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, upsertAdminUser, arg.Email, arg.PasswordHash, arg.Name)
var id uuid.UUID
err := row.Scan(&id)
return id, err
}
const upsertLocalUser = `-- name: UpsertLocalUser :one
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
VALUES ($1, $2, $3, NULLIF($4::varchar, ''), $5)
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name,
avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = NOW()
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
`
type UpsertLocalUserParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
Column4 string `json:"column_4"`
IsAdmin bool `json:"is_admin"`
}
type UpsertLocalUserRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) UpsertLocalUser(ctx context.Context, arg UpsertLocalUserParams) (UpsertLocalUserRow, error) {
row := q.db.QueryRowContext(ctx, upsertLocalUser,
arg.Email,
arg.PasswordHash,
arg.Name,
arg.Column4,
arg.IsAdmin,
)
var i UpsertLocalUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
@@ -1,138 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: builds.sql
package sqlcdb
import (
"context"
"database/sql"
"encoding/json"
"time"
)
const cancelBuild = `-- name: CancelBuild :execrows
UPDATE builds
SET status = 'cancelled',
progress = 100,
completed_at = $1,
log = COALESCE(log, '') || $2,
updated_at = $1
WHERE id = $3
AND status NOT IN ('success', 'failed', 'cancelled')
`
type CancelBuildParams struct {
CompletedAt sql.NullTime `json:"completed_at"`
Log string `json:"log"`
ID string `json:"id"`
}
func (q *Queries) CancelBuild(ctx context.Context, arg CancelBuildParams) (int64, error) {
result, err := q.db.ExecContext(ctx, cancelBuild, arg.CompletedAt, arg.Log, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getBuild = `-- name: GetBuild :one
SELECT id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata
FROM builds
WHERE id = $1
`
type GetBuildRow struct {
ID string `json:"id"`
ProjectID sql.NullString `json:"project_id"`
ServiceID sql.NullString `json:"service_id"`
Status string `json:"status"`
Progress int32 `json:"progress"`
StartedAt time.Time `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
ImageName string `json:"image_name"`
ImageTag string `json:"image_tag"`
Size int64 `json:"size"`
Error sql.NullString `json:"error"`
Log string `json:"log"`
Metadata json.RawMessage `json:"metadata"`
}
func (q *Queries) GetBuild(ctx context.Context, id string) (GetBuildRow, error) {
row := q.db.QueryRowContext(ctx, getBuild, id)
var i GetBuildRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.Status,
&i.Progress,
&i.StartedAt,
&i.CompletedAt,
&i.ImageName,
&i.ImageTag,
&i.Size,
&i.Error,
&i.Log,
&i.Metadata,
)
return i, err
}
const upsertBuild = `-- name: UpsertBuild :exec
INSERT INTO builds
(id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $14)
ON CONFLICT (id) DO UPDATE SET
project_id = EXCLUDED.project_id,
service_id = EXCLUDED.service_id,
status = EXCLUDED.status,
progress = EXCLUDED.progress,
started_at = EXCLUDED.started_at,
completed_at = EXCLUDED.completed_at,
image_name = EXCLUDED.image_name,
image_tag = EXCLUDED.image_tag,
size = EXCLUDED.size,
error = EXCLUDED.error,
log = EXCLUDED.log,
metadata = EXCLUDED.metadata,
updated_at = EXCLUDED.updated_at
`
type UpsertBuildParams struct {
ID string `json:"id"`
ProjectID sql.NullString `json:"project_id"`
ServiceID sql.NullString `json:"service_id"`
Status string `json:"status"`
Progress int32 `json:"progress"`
StartedAt time.Time `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
ImageName string `json:"image_name"`
ImageTag string `json:"image_tag"`
Size int64 `json:"size"`
Error sql.NullString `json:"error"`
Log string `json:"log"`
Column13 json.RawMessage `json:"column_13"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) UpsertBuild(ctx context.Context, arg UpsertBuildParams) error {
_, err := q.db.ExecContext(ctx, upsertBuild,
arg.ID,
arg.ProjectID,
arg.ServiceID,
arg.Status,
arg.Progress,
arg.StartedAt,
arg.CompletedAt,
arg.ImageName,
arg.ImageTag,
arg.Size,
arg.Error,
arg.Log,
arg.Column13,
arg.CreatedAt,
)
return err
}
@@ -1,332 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: cron.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const deleteCronJob = `-- name: DeleteCronJob :exec
DELETE FROM cron_jobs WHERE id = $1
`
func (q *Queries) DeleteCronJob(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteCronJob, id)
return err
}
const finishCronExecution = `-- name: FinishCronExecution :exec
UPDATE cron_executions SET finished_at = $1, status = $2, output = $3, error = $4 WHERE id = $5
`
type FinishCronExecutionParams struct {
FinishedAt sql.NullTime `json:"finished_at"`
Status sql.NullString `json:"status"`
Output sql.NullString `json:"output"`
Error sql.NullString `json:"error"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) FinishCronExecution(ctx context.Context, arg FinishCronExecutionParams) error {
_, err := q.db.ExecContext(ctx, finishCronExecution,
arg.FinishedAt,
arg.Status,
arg.Output,
arg.Error,
arg.ID,
)
return err
}
const getCronJob = `-- name: GetCronJob :one
SELECT id, project_id, service_id, name, schedule, command, timezone,
enabled, last_run_at, next_run_at, last_status, last_output,
retention, created_at, updated_at
FROM cron_jobs WHERE id = $1
`
func (q *Queries) GetCronJob(ctx context.Context, id uuid.UUID) (CronJob, error) {
row := q.db.QueryRowContext(ctx, getCronJob, id)
var i CronJob
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.Name,
&i.Schedule,
&i.Command,
&i.Timezone,
&i.Enabled,
&i.LastRunAt,
&i.NextRunAt,
&i.LastStatus,
&i.LastOutput,
&i.Retention,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getCronJobForTrigger = `-- name: GetCronJobForTrigger :one
SELECT cj.service_id, cj.command, cj.schedule, cj.timezone, cj.retention, p.owner_id
FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1
`
type GetCronJobForTriggerRow struct {
ServiceID uuid.UUID `json:"service_id"`
Command string `json:"command"`
Schedule string `json:"schedule"`
Timezone sql.NullString `json:"timezone"`
Retention sql.NullInt32 `json:"retention"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetCronJobForTrigger(ctx context.Context, id uuid.UUID) (GetCronJobForTriggerRow, error) {
row := q.db.QueryRowContext(ctx, getCronJobForTrigger, id)
var i GetCronJobForTriggerRow
err := row.Scan(
&i.ServiceID,
&i.Command,
&i.Schedule,
&i.Timezone,
&i.Retention,
&i.OwnerID,
)
return i, err
}
const getCronJobOwner = `-- name: GetCronJobOwner :one
SELECT p.owner_id FROM cron_jobs cj
JOIN projects p ON cj.project_id = p.id
WHERE cj.id = $1
`
func (q *Queries) GetCronJobOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getCronJobOwner, id)
var owner_id uuid.UUID
err := row.Scan(&owner_id)
return owner_id, err
}
const getCronJobProjectID = `-- name: GetCronJobProjectID :one
SELECT project_id FROM cron_jobs WHERE id = $1
`
func (q *Queries) GetCronJobProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getCronJobProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getCronOwnerViaService = `-- name: GetCronOwnerViaService :one
SELECT p.owner_id FROM projects p
JOIN services s ON s.project_id = p.id
WHERE s.id = $1
`
func (q *Queries) GetCronOwnerViaService(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getCronOwnerViaService, id)
var owner_id uuid.UUID
err := row.Scan(&owner_id)
return owner_id, err
}
const insertCronExecution = `-- name: InsertCronExecution :exec
INSERT INTO cron_executions (id, cron_job_id, started_at, status)
VALUES ($1, $2, $3, $4)
`
type InsertCronExecutionParams struct {
ID uuid.UUID `json:"id"`
CronJobID uuid.UUID `json:"cron_job_id"`
StartedAt time.Time `json:"started_at"`
Status sql.NullString `json:"status"`
}
func (q *Queries) InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error {
_, err := q.db.ExecContext(ctx, insertCronExecution,
arg.ID,
arg.CronJobID,
arg.StartedAt,
arg.Status,
)
return err
}
const insertCronJob = `-- name: InsertCronJob :exec
INSERT INTO cron_jobs (id, project_id, service_id, name, schedule, command, timezone, enabled, next_run_at, retention, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`
type InsertCronJobParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
Name string `json:"name"`
Schedule string `json:"schedule"`
Command string `json:"command"`
Timezone sql.NullString `json:"timezone"`
Enabled sql.NullBool `json:"enabled"`
NextRunAt sql.NullTime `json:"next_run_at"`
Retention sql.NullInt32 `json:"retention"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) InsertCronJob(ctx context.Context, arg InsertCronJobParams) error {
_, err := q.db.ExecContext(ctx, insertCronJob,
arg.ID,
arg.ProjectID,
arg.ServiceID,
arg.Name,
arg.Schedule,
arg.Command,
arg.Timezone,
arg.Enabled,
arg.NextRunAt,
arg.Retention,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const listCronExecutions = `-- name: ListCronExecutions :many
SELECT id, cron_job_id, started_at, finished_at, status, output, error
FROM cron_executions
WHERE cron_job_id = $1
ORDER BY started_at DESC
LIMIT 100
`
func (q *Queries) ListCronExecutions(ctx context.Context, cronJobID uuid.UUID) ([]CronExecution, error) {
rows, err := q.db.QueryContext(ctx, listCronExecutions, cronJobID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []CronExecution{}
for rows.Next() {
var i CronExecution
if err := rows.Scan(
&i.ID,
&i.CronJobID,
&i.StartedAt,
&i.FinishedAt,
&i.Status,
&i.Output,
&i.Error,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listDueCronJobs = `-- name: ListDueCronJobs :many
SELECT id, project_id, service_id, name, schedule, command, timezone, enabled, retention
FROM cron_jobs
WHERE enabled = TRUE AND next_run_at IS NOT NULL AND next_run_at <= NOW()
`
type ListDueCronJobsRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
Name string `json:"name"`
Schedule string `json:"schedule"`
Command string `json:"command"`
Timezone sql.NullString `json:"timezone"`
Enabled sql.NullBool `json:"enabled"`
Retention sql.NullInt32 `json:"retention"`
}
func (q *Queries) ListDueCronJobs(ctx context.Context) ([]ListDueCronJobsRow, error) {
rows, err := q.db.QueryContext(ctx, listDueCronJobs)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListDueCronJobsRow{}
for rows.Next() {
var i ListDueCronJobsRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.ServiceID,
&i.Name,
&i.Schedule,
&i.Command,
&i.Timezone,
&i.Enabled,
&i.Retention,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const trimCronExecutions = `-- name: TrimCronExecutions :exec
DELETE FROM cron_executions WHERE cron_executions.cron_job_id = $1 AND cron_executions.id NOT IN (
SELECT ce.id FROM cron_executions ce WHERE ce.cron_job_id = $1
ORDER BY ce.started_at DESC LIMIT $2)
`
type TrimCronExecutionsParams struct {
CronJobID uuid.UUID `json:"cron_job_id"`
Limit int32 `json:"limit"`
}
func (q *Queries) TrimCronExecutions(ctx context.Context, arg TrimCronExecutionsParams) error {
_, err := q.db.ExecContext(ctx, trimCronExecutions, arg.CronJobID, arg.Limit)
return err
}
const updateCronJobRun = `-- name: UpdateCronJobRun :exec
UPDATE cron_jobs SET last_run_at = $1, last_status = $2, last_output = $3, next_run_at = $4 WHERE id = $5
`
type UpdateCronJobRunParams struct {
LastRunAt sql.NullTime `json:"last_run_at"`
LastStatus sql.NullString `json:"last_status"`
LastOutput sql.NullString `json:"last_output"`
NextRunAt sql.NullTime `json:"next_run_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateCronJobRun(ctx context.Context, arg UpdateCronJobRunParams) error {
_, err := q.db.ExecContext(ctx, updateCronJobRun,
arg.LastRunAt,
arg.LastStatus,
arg.LastOutput,
arg.NextRunAt,
arg.ID,
)
return err
}
@@ -1,536 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: deployments.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const completeDeployment = `-- name: CompleteDeployment :exec
UPDATE deployments
SET status = $1, error = $2, completed_at = $3, updated_at = $3
WHERE id = $4
`
type CompleteDeploymentParams struct {
Status sql.NullString `json:"status"`
Error sql.NullString `json:"error"`
CompletedAt sql.NullTime `json:"completed_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error {
_, err := q.db.ExecContext(ctx, completeDeployment,
arg.Status,
arg.Error,
arg.CompletedAt,
arg.ID,
)
return err
}
const countAccessibleDeployments = `-- name: CountAccessibleDeployments :one
SELECT COUNT(*)
FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)
`
type CountAccessibleDeploymentsParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Column2 bool `json:"column_2"`
}
func (q *Queries) CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countAccessibleDeployments, arg.OwnerID, arg.Column2)
var count int64
err := row.Scan(&count)
return count, err
}
const countDeploymentsForService = `-- name: CountDeploymentsForService :one
SELECT COUNT(*) FROM deployments WHERE service_id = $1
`
func (q *Queries) CountDeploymentsForService(ctx context.Context, serviceID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, countDeploymentsForService, serviceID)
var count int64
err := row.Scan(&count)
return count, err
}
const failDeployment = `-- name: FailDeployment :exec
UPDATE deployments
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
WHERE id = $3
`
type FailDeploymentParams struct {
Error sql.NullString `json:"error"`
CompletedAt sql.NullTime `json:"completed_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) FailDeployment(ctx context.Context, arg FailDeploymentParams) error {
_, err := q.db.ExecContext(ctx, failDeployment, arg.Error, arg.CompletedAt, arg.ID)
return err
}
const getDeploymentAccess = `-- name: GetDeploymentAccess :one
SELECT d.status, d.service_id, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1
`
type GetDeploymentAccessRow struct {
Status sql.NullString `json:"status"`
ServiceID uuid.UUID `json:"service_id"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetDeploymentAccess(ctx context.Context, id uuid.UUID) (GetDeploymentAccessRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentAccess, id)
var i GetDeploymentAccessRow
err := row.Scan(&i.Status, &i.ServiceID, &i.OwnerID)
return i, err
}
const getDeploymentForRollback = `-- name: GetDeploymentForRollback :one
SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.owner_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1
`
type GetDeploymentForRollbackRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
CommitHash sql.NullString `json:"commit_hash"`
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
BuildLog sql.NullString `json:"build_log"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentForRollback, id)
var i GetDeploymentForRollbackRow
err := row.Scan(
&i.ID,
&i.ServiceID,
&i.CommitHash,
&i.Status,
&i.ImageName,
&i.ImageTag,
&i.BuildLog,
&i.RuntimeLog,
&i.Error,
&i.StartedAt,
&i.CompletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
const getDeploymentWithProject = `-- name: GetDeploymentWithProject :one
SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
d.created_at, d.updated_at, p.id AS project_id
FROM deployments d
JOIN services s ON d.service_id = s.id
JOIN projects p ON s.project_id = p.id
WHERE d.id = $1
`
type GetDeploymentWithProjectRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
CommitHash sql.NullString `json:"commit_hash"`
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
BuildLog sql.NullString `json:"build_log"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
ProjectID uuid.UUID `json:"project_id"`
}
func (q *Queries) GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error) {
row := q.db.QueryRowContext(ctx, getDeploymentWithProject, id)
var i GetDeploymentWithProjectRow
err := row.Scan(
&i.ID,
&i.ServiceID,
&i.CommitHash,
&i.Status,
&i.ImageName,
&i.ImageTag,
&i.BuildLog,
&i.RuntimeLog,
&i.Error,
&i.StartedAt,
&i.CompletedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.ProjectID,
)
return i, err
}
const getServiceForDeployWithOwner = `-- name: GetServiceForDeployWithOwner :one
SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
COALESCE(s.replicas, 1) AS replicas, COALESCE(s.port, 0) AS port,
COALESCE(s.domain, '') AS domain, COALESCE(s.healthcheck_path, '') AS healthcheck_path,
COALESCE(s.restart_policy, 'unless-stopped') AS restart_policy,
COALESCE(s.builder, 'auto') AS builder, COALESCE(s.cpu_reserve, '') AS cpu_reserve,
COALESCE(s.memory_reserve, '') AS memory_reserve, COALESCE(s.static_build_cmd, '') AS static_build_cmd,
COALESCE(s.static_dir, '') AS static_dir,
s.created_at, s.updated_at, p.owner_id
FROM services s
JOIN projects p ON s.project_id = p.id
WHERE s.id = $1
`
type GetServiceForDeployWithOwnerRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Type sql.NullString `json:"type"`
Status sql.NullString `json:"status"`
Image sql.NullString `json:"image"`
Command sql.NullString `json:"command"`
Environment sql.NullString `json:"environment"`
GitRepo sql.NullString `json:"git_repo"`
GitBranch sql.NullString `json:"git_branch"`
BuildPath sql.NullString `json:"build_path"`
Cpu sql.NullString `json:"cpu"`
Memory sql.NullString `json:"memory"`
Replicas int32 `json:"replicas"`
Port int32 `json:"port"`
Domain string `json:"domain"`
HealthcheckPath string `json:"healthcheck_path"`
RestartPolicy string `json:"restart_policy"`
Builder string `json:"builder"`
CpuReserve string `json:"cpu_reserve"`
MemoryReserve string `json:"memory_reserve"`
StaticBuildCmd string `json:"static_build_cmd"`
StaticDir string `json:"static_dir"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetServiceForDeployWithOwner(ctx context.Context, id uuid.UUID) (GetServiceForDeployWithOwnerRow, error) {
row := q.db.QueryRowContext(ctx, getServiceForDeployWithOwner, id)
var i GetServiceForDeployWithOwnerRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Type,
&i.Status,
&i.Image,
&i.Command,
&i.Environment,
&i.GitRepo,
&i.GitBranch,
&i.BuildPath,
&i.Cpu,
&i.Memory,
&i.Replicas,
&i.Port,
&i.Domain,
&i.HealthcheckPath,
&i.RestartPolicy,
&i.Builder,
&i.CpuReserve,
&i.MemoryReserve,
&i.StaticBuildCmd,
&i.StaticDir,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
const getServicePublishedPort = `-- name: GetServicePublishedPort :one
SELECT published_port FROM services WHERE id = $1
`
func (q *Queries) GetServicePublishedPort(ctx context.Context, id uuid.UUID) (int32, error) {
row := q.db.QueryRowContext(ctx, getServicePublishedPort, id)
var published_port int32
err := row.Scan(&published_port)
return published_port, err
}
const insertDeployment = `-- name: InsertDeployment :exec
INSERT INTO deployments
(id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
type InsertDeploymentParams struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
Version string `json:"version"`
CommitHash sql.NullString `json:"commit_hash"`
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) InsertDeployment(ctx context.Context, arg InsertDeploymentParams) error {
_, err := q.db.ExecContext(ctx, insertDeployment,
arg.ID,
arg.ServiceID,
arg.Version,
arg.CommitHash,
arg.Status,
arg.ImageName,
arg.ImageTag,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const listDeploymentsForService = `-- name: ListDeploymentsForService :many
SELECT id, service_id, commit_hash, status, image_name, image_tag,
build_log, runtime_log, error, started_at, completed_at, created_at, updated_at
FROM deployments
WHERE service_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
type ListDeploymentsForServiceParams struct {
ServiceID uuid.UUID `json:"service_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListDeploymentsForServiceRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
CommitHash sql.NullString `json:"commit_hash"`
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
BuildLog sql.NullString `json:"build_log"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) ListDeploymentsForService(ctx context.Context, arg ListDeploymentsForServiceParams) ([]ListDeploymentsForServiceRow, error) {
rows, err := q.db.QueryContext(ctx, listDeploymentsForService, arg.ServiceID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListDeploymentsForServiceRow{}
for rows.Next() {
var i ListDeploymentsForServiceRow
if err := rows.Scan(
&i.ID,
&i.ServiceID,
&i.CommitHash,
&i.Status,
&i.ImageName,
&i.ImageTag,
&i.BuildLog,
&i.RuntimeLog,
&i.Error,
&i.StartedAt,
&i.CompletedAt,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRecentAccessibleDeployments = `-- name: ListRecentAccessibleDeployments :many
SELECT d.id, d.service_id, s.name AS service_name, p.name AS project_name, d.status,
COALESCE(d.image_name, '') AS image_name, d.started_at, d.completed_at, d.created_at
FROM deployments d
JOIN services s ON s.id = d.service_id
JOIN projects p ON p.id = s.project_id
WHERE p.is_approved OR p.owner_id = $1 OR $2::bool
OR EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = $1)
ORDER BY d.created_at DESC
LIMIT $3 OFFSET $4
`
type ListRecentAccessibleDeploymentsParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Column2 bool `json:"column_2"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListRecentAccessibleDeploymentsRow struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
ServiceName string `json:"service_name"`
ProjectName string `json:"project_name"`
Status sql.NullString `json:"status"`
ImageName string `json:"image_name"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) ListRecentAccessibleDeployments(ctx context.Context, arg ListRecentAccessibleDeploymentsParams) ([]ListRecentAccessibleDeploymentsRow, error) {
rows, err := q.db.QueryContext(ctx, listRecentAccessibleDeployments,
arg.OwnerID,
arg.Column2,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListRecentAccessibleDeploymentsRow{}
for rows.Next() {
var i ListRecentAccessibleDeploymentsRow
if err := rows.Scan(
&i.ID,
&i.ServiceID,
&i.ServiceName,
&i.ProjectName,
&i.Status,
&i.ImageName,
&i.StartedAt,
&i.CompletedAt,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setDeploymentStatus = `-- name: SetDeploymentStatus :exec
UPDATE deployments SET status = $1, updated_at = $2 WHERE id = $3
`
type SetDeploymentStatusParams struct {
Status sql.NullString `json:"status"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetDeploymentStatus(ctx context.Context, arg SetDeploymentStatusParams) error {
_, err := q.db.ExecContext(ctx, setDeploymentStatus, arg.Status, arg.UpdatedAt, arg.ID)
return err
}
const setServiceStatus = `-- name: SetServiceStatus :exec
UPDATE services SET status = $1, updated_at = $2 WHERE id = $3
`
type SetServiceStatusParams struct {
Status sql.NullString `json:"status"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetServiceStatus(ctx context.Context, arg SetServiceStatusParams) error {
_, err := q.db.ExecContext(ctx, setServiceStatus, arg.Status, arg.UpdatedAt, arg.ID)
return err
}
const syncDeploymentProgress = `-- name: SyncDeploymentProgress :exec
UPDATE deployments
SET status = $1,
image_name = $2,
image_tag = $3,
build_log = $4,
runtime_log = $5,
error = $6,
started_at = $7,
completed_at = $8,
updated_at = $9
WHERE id = $10
`
type SyncDeploymentProgressParams struct {
Status sql.NullString `json:"status"`
ImageName sql.NullString `json:"image_name"`
ImageTag sql.NullString `json:"image_tag"`
BuildLog sql.NullString `json:"build_log"`
RuntimeLog sql.NullString `json:"runtime_log"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SyncDeploymentProgress(ctx context.Context, arg SyncDeploymentProgressParams) error {
_, err := q.db.ExecContext(ctx, syncDeploymentProgress,
arg.Status,
arg.ImageName,
arg.ImageTag,
arg.BuildLog,
arg.RuntimeLog,
arg.Error,
arg.StartedAt,
arg.CompletedAt,
arg.UpdatedAt,
arg.ID,
)
return err
}
@@ -1,776 +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 getGitRepoCloneByFullName = `-- name: GetGitRepoCloneByFullName :one
SELECT clone_url, provider_id FROM git_repositories
WHERE full_name = $1 AND user_id = $2 LIMIT 1
`
type GetGitRepoCloneByFullNameParams struct {
FullName string `json:"full_name"`
UserID uuid.UUID `json:"user_id"`
}
type GetGitRepoCloneByFullNameRow struct {
CloneUrl string `json:"clone_url"`
ProviderID uuid.UUID `json:"provider_id"`
}
func (q *Queries) GetGitRepoCloneByFullName(ctx context.Context, arg GetGitRepoCloneByFullNameParams) (GetGitRepoCloneByFullNameRow, error) {
row := q.db.QueryRowContext(ctx, getGitRepoCloneByFullName, arg.FullName, arg.UserID)
var i GetGitRepoCloneByFullNameRow
err := row.Scan(&i.CloneUrl, &i.ProviderID)
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 getGitRepoProviderByCloneURL = `-- name: GetGitRepoProviderByCloneURL :one
SELECT provider_id FROM git_repositories
WHERE clone_url = $1 AND user_id = $2 LIMIT 1
`
type GetGitRepoProviderByCloneURLParams struct {
CloneUrl string `json:"clone_url"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) GetGitRepoProviderByCloneURL(ctx context.Context, arg GetGitRepoProviderByCloneURLParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getGitRepoProviderByCloneURL, arg.CloneUrl, arg.UserID)
var provider_id uuid.UUID
err := row.Scan(&provider_id)
return provider_id, err
}
const getGitRepositoryByFullName = `-- name: GetGitRepositoryByFullName :one
SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description,
clone_url, COALESCE(webhook_url, '') AS webhook_url,
default_branch, is_private, user_id, created_at, updated_at
FROM git_repositories
WHERE provider_id = $1 AND full_name = $2 AND user_id = $3
`
type GetGitRepositoryByFullNameParams struct {
ProviderID uuid.UUID `json:"provider_id"`
FullName string `json:"full_name"`
UserID uuid.UUID `json:"user_id"`
}
type GetGitRepositoryByFullNameRow struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
CloneUrl string `json:"clone_url"`
WebhookUrl string `json:"webhook_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) GetGitRepositoryByFullName(ctx context.Context, arg GetGitRepositoryByFullNameParams) (GetGitRepositoryByFullNameRow, error) {
row := q.db.QueryRowContext(ctx, getGitRepositoryByFullName, arg.ProviderID, arg.FullName, arg.UserID)
var i GetGitRepositoryByFullNameRow
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.Name,
&i.FullName,
&i.Description,
&i.CloneUrl,
&i.WebhookUrl,
&i.DefaultBranch,
&i.IsPrivate,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getGitRepositoryProviderID = `-- name: GetGitRepositoryProviderID :one
SELECT provider_id FROM git_repositories WHERE id = $1 AND user_id = $2
`
type GetGitRepositoryProviderIDParams struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) GetGitRepositoryProviderID(ctx context.Context, arg GetGitRepositoryProviderIDParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getGitRepositoryProviderID, arg.ID, arg.UserID)
var provider_id uuid.UUID
err := row.Scan(&provider_id)
return provider_id, err
}
const getGitWebhookForPush = `-- name: GetGitWebhookForPush :one
SELECT webhook_secret, repo_id, provider_id, COALESCE(branch_filter, '') AS branch_filter, active
FROM git_webhooks WHERE id = $1
`
type GetGitWebhookForPushRow struct {
WebhookSecret string `json:"webhook_secret"`
RepoID uuid.UUID `json:"repo_id"`
ProviderID uuid.UUID `json:"provider_id"`
BranchFilter string `json:"branch_filter"`
Active sql.NullBool `json:"active"`
}
func (q *Queries) GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error) {
row := q.db.QueryRowContext(ctx, getGitWebhookForPush, id)
var i GetGitWebhookForPushRow
err := row.Scan(
&i.WebhookSecret,
&i.RepoID,
&i.ProviderID,
&i.BranchFilter,
&i.Active,
)
return i, err
}
const insertGitProvider = `-- name: InsertGitProvider :exec
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
`
type InsertGitProviderParams struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
ApiUrl string `json:"api_url"`
WebhookUrl string `json:"webhook_url"`
AccessToken string `json:"access_token"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) InsertGitProvider(ctx context.Context, arg InsertGitProviderParams) error {
_, err := q.db.ExecContext(ctx, insertGitProvider,
arg.ID,
arg.Name,
arg.DisplayName,
arg.ApiUrl,
arg.WebhookUrl,
arg.AccessToken,
arg.UserID,
)
return err
}
const insertGitRepository = `-- name: InsertGitRepository :one
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
RETURNING created_at, updated_at
`
type InsertGitRepositoryParams struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description sql.NullString `json:"description"`
CloneUrl string `json:"clone_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
}
type InsertGitRepositoryRow struct {
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) InsertGitRepository(ctx context.Context, arg InsertGitRepositoryParams) (InsertGitRepositoryRow, error) {
row := q.db.QueryRowContext(ctx, insertGitRepository,
arg.ID,
arg.ProviderID,
arg.Name,
arg.FullName,
arg.Description,
arg.CloneUrl,
arg.DefaultBranch,
arg.IsPrivate,
arg.UserID,
)
var i InsertGitRepositoryRow
err := row.Scan(&i.CreatedAt, &i.UpdatedAt)
return i, err
}
const listConnectedRepositories = `-- name: ListConnectedRepositories :many
SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.description, '') AS description, r.clone_url,
r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at,
p.name AS provider_name, p.display_name
FROM git_repositories r
JOIN git_providers p ON r.provider_id = p.id
WHERE r.user_id = $1
ORDER BY r.updated_at DESC
LIMIT $2 OFFSET $3
`
type ListConnectedRepositoriesParams struct {
UserID uuid.UUID `json:"user_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListConnectedRepositoriesRow struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
CloneUrl string `json:"clone_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
ProviderName string `json:"provider_name"`
DisplayName string `json:"display_name"`
}
func (q *Queries) ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error) {
rows, err := q.db.QueryContext(ctx, listConnectedRepositories, arg.UserID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListConnectedRepositoriesRow{}
for rows.Next() {
var i ListConnectedRepositoriesRow
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.Name,
&i.FullName,
&i.Description,
&i.CloneUrl,
&i.DefaultBranch,
&i.IsPrivate,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.ProviderName,
&i.DisplayName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listGitProviders = `-- name: ListGitProviders :many
SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at
FROM git_providers
WHERE user_id = $1
ORDER BY created_at DESC
`
type ListGitProvidersRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
ApiUrl string `json:"api_url"`
WebhookUrl string `json:"webhook_url"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) ListGitProviders(ctx context.Context, userID uuid.UUID) ([]ListGitProvidersRow, error) {
rows, err := q.db.QueryContext(ctx, listGitProviders, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListGitProvidersRow{}
for rows.Next() {
var i ListGitProvidersRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.DisplayName,
&i.ApiUrl,
&i.WebhookUrl,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listGitRepositories = `-- name: ListGitRepositories :many
SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description,
clone_url, COALESCE(webhook_url, '') AS webhook_url,
default_branch, is_private, user_id, created_at, updated_at
FROM git_repositories
WHERE provider_id = $1 AND user_id = $2
AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%'))
ORDER BY updated_at DESC
LIMIT $4 OFFSET $5
`
type ListGitRepositoriesParams struct {
ProviderID uuid.UUID `json:"provider_id"`
UserID uuid.UUID `json:"user_id"`
Column3 string `json:"column_3"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type ListGitRepositoriesRow struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
CloneUrl string `json:"clone_url"`
WebhookUrl string `json:"webhook_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) ListGitRepositories(ctx context.Context, arg ListGitRepositoriesParams) ([]ListGitRepositoriesRow, error) {
rows, err := q.db.QueryContext(ctx, listGitRepositories,
arg.ProviderID,
arg.UserID,
arg.Column3,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListGitRepositoriesRow{}
for rows.Next() {
var i ListGitRepositoriesRow
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.Name,
&i.FullName,
&i.Description,
&i.CloneUrl,
&i.WebhookUrl,
&i.DefaultBranch,
&i.IsPrivate,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listServicesForPush = `-- name: ListServicesForPush :many
SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status,
COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command,
COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo,
COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path,
COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS memory,
s.created_at, s.updated_at
FROM services s
WHERE (s.git_branch = $1 OR $1 = '') AND (s.git_repo = $2 OR s.git_repo = $3)
`
type ListServicesForPushParams struct {
GitBranch sql.NullString `json:"git_branch"`
GitRepo sql.NullString `json:"git_repo"`
GitRepo_2 sql.NullString `json:"git_repo_2"`
}
type ListServicesForPushRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
Image string `json:"image"`
Command string `json:"command"`
Environment string `json:"environment"`
GitRepo string `json:"git_repo"`
GitBranch string `json:"git_branch"`
BuildPath string `json:"build_path"`
Cpu string `json:"cpu"`
Memory string `json:"memory"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) ListServicesForPush(ctx context.Context, arg ListServicesForPushParams) ([]ListServicesForPushRow, error) {
rows, err := q.db.QueryContext(ctx, listServicesForPush, arg.GitBranch, arg.GitRepo, arg.GitRepo_2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListServicesForPushRow{}
for rows.Next() {
var i ListServicesForPushRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Type,
&i.Status,
&i.Image,
&i.Command,
&i.Environment,
&i.GitRepo,
&i.GitBranch,
&i.BuildPath,
&i.Cpu,
&i.Memory,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertGitHubAppProvider = `-- name: UpsertGitHubAppProvider :one
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
ON CONFLICT (name, user_id)
DO UPDATE SET
display_name = EXCLUDED.display_name,
api_url = EXCLUDED.api_url,
webhook_url = EXCLUDED.webhook_url,
access_token = EXCLUDED.access_token,
updated_at = NOW()
RETURNING id, created_at, updated_at
`
type UpsertGitHubAppProviderParams struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
ApiUrl string `json:"api_url"`
WebhookUrl string `json:"webhook_url"`
AccessToken string `json:"access_token"`
UserID uuid.UUID `json:"user_id"`
}
type UpsertGitHubAppProviderRow struct {
ID uuid.UUID `json:"id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error) {
row := q.db.QueryRowContext(ctx, upsertGitHubAppProvider,
arg.ID,
arg.Name,
arg.DisplayName,
arg.ApiUrl,
arg.WebhookUrl,
arg.AccessToken,
arg.UserID,
)
var i UpsertGitHubAppProviderRow
err := row.Scan(&i.ID, &i.CreatedAt, &i.UpdatedAt)
return i, err
}
const upsertGitRepository = `-- name: UpsertGitRepository :exec
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
ON CONFLICT (provider_id, full_name)
DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
clone_url = EXCLUDED.clone_url,
default_branch = EXCLUDED.default_branch,
is_private = EXCLUDED.is_private,
updated_at = NOW()
`
type UpsertGitRepositoryParams struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description sql.NullString `json:"description"`
CloneUrl string `json:"clone_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error {
_, err := q.db.ExecContext(ctx, upsertGitRepository,
arg.ID,
arg.ProviderID,
arg.Name,
arg.FullName,
arg.Description,
arg.CloneUrl,
arg.DefaultBranch,
arg.IsPrivate,
arg.UserID,
)
return err
}
const upsertGitWebhook = `-- name: UpsertGitWebhook :one
INSERT INTO git_webhooks (id, repo_id, provider_id, events, webhook_secret, active, branch_filter, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, TRUE, $6, NOW(), NOW())
ON CONFLICT (repo_id, provider_id)
DO UPDATE SET
events = EXCLUDED.events,
webhook_secret = EXCLUDED.webhook_secret,
active = TRUE,
branch_filter = EXCLUDED.branch_filter,
updated_at = NOW()
RETURNING id, active, created_at, updated_at
`
type UpsertGitWebhookParams struct {
ID uuid.UUID `json:"id"`
RepoID uuid.UUID `json:"repo_id"`
ProviderID uuid.UUID `json:"provider_id"`
Events string `json:"events"`
WebhookSecret string `json:"webhook_secret"`
BranchFilter sql.NullString `json:"branch_filter"`
}
type UpsertGitWebhookRow struct {
ID uuid.UUID `json:"id"`
Active sql.NullBool `json:"active"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) UpsertGitWebhook(ctx context.Context, arg UpsertGitWebhookParams) (UpsertGitWebhookRow, error) {
row := q.db.QueryRowContext(ctx, upsertGitWebhook,
arg.ID,
arg.RepoID,
arg.ProviderID,
arg.Events,
arg.WebhookSecret,
arg.BranchFilter,
)
var i UpsertGitWebhookRow
err := row.Scan(
&i.ID,
&i.Active,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
@@ -1,174 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: misc.sql
package sqlcdb
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/google/uuid"
)
const countProjects = `-- name: CountProjects :one
SELECT COUNT(*) FROM projects
`
func (q *Queries) CountProjects(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, countProjects)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteProjectVariables = `-- name: DeleteProjectVariables :exec
DELETE FROM project_variables WHERE project_id = $1
`
func (q *Queries) DeleteProjectVariables(ctx context.Context, projectID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteProjectVariables, projectID)
return err
}
const getProjectVariableValue = `-- name: GetProjectVariableValue :one
SELECT value FROM project_variables WHERE project_id = $1 AND key = $2
`
type GetProjectVariableValueParams struct {
ProjectID uuid.UUID `json:"project_id"`
Key string `json:"key"`
}
func (q *Queries) GetProjectVariableValue(ctx context.Context, arg GetProjectVariableValueParams) (string, error) {
row := q.db.QueryRowContext(ctx, getProjectVariableValue, arg.ProjectID, arg.Key)
var value string
err := row.Scan(&value)
return value, err
}
const getRegistryAuth = `-- name: GetRegistryAuth :one
SELECT username, password FROM registries
WHERE owner_id::text = $1 AND host = $2
`
type GetRegistryAuthParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Host string `json:"host"`
}
type GetRegistryAuthRow struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (q *Queries) GetRegistryAuth(ctx context.Context, arg GetRegistryAuthParams) (GetRegistryAuthRow, error) {
row := q.db.QueryRowContext(ctx, getRegistryAuth, arg.OwnerID, arg.Host)
var i GetRegistryAuthRow
err := row.Scan(&i.Username, &i.Password)
return i, err
}
const insertEnvVar = `-- name: InsertEnvVar :exec
INSERT INTO environment_variables (id, service_id, key, value, is_secret, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`
type InsertEnvVarParams struct {
ID uuid.UUID `json:"id"`
ServiceID uuid.UUID `json:"service_id"`
Key string `json:"key"`
Value string `json:"value"`
IsSecret sql.NullBool `json:"is_secret"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
func (q *Queries) InsertEnvVar(ctx context.Context, arg InsertEnvVarParams) error {
_, err := q.db.ExecContext(ctx, insertEnvVar,
arg.ID,
arg.ServiceID,
arg.Key,
arg.Value,
arg.IsSecret,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const insertProjectVariable = `-- name: InsertProjectVariable :exec
INSERT INTO project_variables (id, project_id, key, value, is_secret, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`
type InsertProjectVariableParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Key string `json:"key"`
Value string `json:"value"`
IsSecret bool `json:"is_secret"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (q *Queries) InsertProjectVariable(ctx context.Context, arg InsertProjectVariableParams) error {
_, err := q.db.ExecContext(ctx, insertProjectVariable,
arg.ID,
arg.ProjectID,
arg.Key,
arg.Value,
arg.IsSecret,
arg.CreatedAt,
arg.UpdatedAt,
)
return err
}
const updateServiceDeployMeta = `-- name: UpdateServiceDeployMeta :exec
UPDATE services SET port = $1, healthcheck_path = $2,
volumes = COALESCE($3::jsonb, '[]'::jsonb),
git_repo = NULLIF($4::text, ''), git_branch = NULLIF($5::text, ''), builder = $6
WHERE id = $7
`
type UpdateServiceDeployMetaParams struct {
Port int32 `json:"port"`
HealthcheckPath string `json:"healthcheck_path"`
Column3 json.RawMessage `json:"column_3"`
Column4 string `json:"column_4"`
Column5 string `json:"column_5"`
Builder string `json:"builder"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateServiceDeployMeta(ctx context.Context, arg UpdateServiceDeployMetaParams) error {
_, err := q.db.ExecContext(ctx, updateServiceDeployMeta,
arg.Port,
arg.HealthcheckPath,
arg.Column3,
arg.Column4,
arg.Column5,
arg.Builder,
arg.ID,
)
return err
}
const updateServiceRuntimeMeta = `-- name: UpdateServiceRuntimeMeta :exec
UPDATE services SET port = $1, healthcheck_path = $2 WHERE id = $3
`
type UpdateServiceRuntimeMetaParams struct {
Port int32 `json:"port"`
HealthcheckPath string `json:"healthcheck_path"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateServiceRuntimeMeta(ctx context.Context, arg UpdateServiceRuntimeMetaParams) error {
_, err := q.db.ExecContext(ctx, updateServiceRuntimeMeta, arg.Port, arg.HealthcheckPath, arg.ID)
return err
}
+45 -293
View File
@@ -49,66 +49,6 @@ type AgentHeartbeat struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
type ApiKey struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
KeyHash string `json:"key_hash"`
KeyPrefix string `json:"key_prefix"`
Plan string `json:"plan"`
AllowedServiceIds string `json:"allowed_service_ids"`
Enabled int32 `json:"enabled"`
RpmLimit sql.NullInt32 `json:"rpm_limit"`
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastUsedAt sql.NullTime `json:"last_used_at"`
}
type ApiService struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
UpstreamUrl string `json:"upstream_url"`
RoutePrefix string `json:"route_prefix"`
HealthPath string `json:"health_path"`
UpstreamAuthHeader sql.NullString `json:"upstream_auth_header"`
UpstreamAuthValue sql.NullString `json:"upstream_auth_value"`
InternalToken sql.NullString `json:"internal_token"`
Enabled int32 `json:"enabled"`
RpmLimit sql.NullInt32 `json:"rpm_limit"`
MonthlyQuota sql.NullInt32 `json:"monthly_quota"`
RequestTimeoutMs sql.NullInt32 `json:"request_timeout_ms"`
LastValidationAt sql.NullTime `json:"last_validation_at"`
LastValidationStatus sql.NullString `json:"last_validation_status"`
LastValidationMessage sql.NullString `json:"last_validation_message"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type AppSetting struct {
Key string `json:"key"`
Value string `json:"value"`
IsSecret bool `json:"is_secret"`
UpdatedAt time.Time `json:"updated_at"`
}
type AuditLog struct {
ID uuid.UUID `json:"id"`
Timestamp time.Time `json:"timestamp"`
UserID uuid.NullUUID `json:"user_id"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID uuid.NullUUID `json:"resource_id"`
Details pqtype.NullRawMessage `json:"details"`
IpAddress pqtype.Inet `json:"ip_address"`
UserAgent sql.NullString `json:"user_agent"`
Success bool `json:"success"`
CreatedAt time.Time `json:"created_at"`
Severity string `json:"severity"`
Category string `json:"category"`
Label string `json:"label"`
}
type BackupTarget struct { type BackupTarget struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
@@ -138,46 +78,6 @@ type Banner struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type Build struct {
ID string `json:"id"`
ProjectID sql.NullString `json:"project_id"`
ServiceID sql.NullString `json:"service_id"`
Status string `json:"status"`
Progress int32 `json:"progress"`
StartedAt time.Time `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
ImageName string `json:"image_name"`
ImageTag string `json:"image_tag"`
Size int64 `json:"size"`
Error sql.NullString `json:"error"`
Log string `json:"log"`
Metadata json.RawMessage `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ComplianceFramework struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description sql.NullString `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ComplianceReport struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
FrameworkID uuid.UUID `json:"framework_id"`
AssessmentDate time.Time `json:"assessment_date"`
Assessor sql.NullString `json:"assessor"`
OverallStatus string `json:"overall_status"`
Score int32 `json:"score"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ContainerInstance struct { type ContainerInstance struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -198,34 +98,6 @@ type ContainerInstance struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type CronExecution struct {
ID uuid.UUID `json:"id"`
CronJobID uuid.UUID `json:"cron_job_id"`
StartedAt time.Time `json:"started_at"`
FinishedAt sql.NullTime `json:"finished_at"`
Status sql.NullString `json:"status"`
Output sql.NullString `json:"output"`
Error sql.NullString `json:"error"`
}
type CronJob struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.UUID `json:"service_id"`
Name string `json:"name"`
Schedule string `json:"schedule"`
Command string `json:"command"`
Timezone sql.NullString `json:"timezone"`
Enabled sql.NullBool `json:"enabled"`
LastRunAt sql.NullTime `json:"last_run_at"`
NextRunAt sql.NullTime `json:"next_run_at"`
LastStatus sql.NullString `json:"last_status"`
LastOutput sql.NullString `json:"last_output"`
Retention sql.NullInt32 `json:"retention"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type DatabaseBackup struct { type DatabaseBackup struct {
ID string `json:"id"` ID string `json:"id"`
DatabaseID string `json:"database_id"` DatabaseID string `json:"database_id"`
@@ -300,58 +172,6 @@ type EnvironmentVariable struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type GitProvider struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
ApiUrl string `json:"api_url"`
WebhookUrl string `json:"webhook_url"`
AccessToken string `json:"access_token"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type GitRepository struct {
ID uuid.UUID `json:"id"`
ProviderID uuid.UUID `json:"provider_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description sql.NullString `json:"description"`
CloneUrl string `json:"clone_url"`
WebhookUrl sql.NullString `json:"webhook_url"`
DefaultBranch sql.NullString `json:"default_branch"`
IsPrivate sql.NullBool `json:"is_private"`
UserID uuid.UUID `json:"user_id"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type GitWebhook struct {
ID uuid.UUID `json:"id"`
RepoID uuid.UUID `json:"repo_id"`
ProviderID uuid.UUID `json:"provider_id"`
Events string `json:"events"`
WebhookSecret string `json:"webhook_secret"`
RemoteWebhookID sql.NullString `json:"remote_webhook_id"`
Active sql.NullBool `json:"active"`
BranchFilter sql.NullString `json:"branch_filter"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type IncidentEvent struct {
ID int32 `json:"id"`
ServiceID uuid.NullUUID `json:"service_id"`
ApiKeyID uuid.NullUUID `json:"api_key_id"`
Code string `json:"code"`
Message string `json:"message"`
Severity string `json:"severity"`
HttpStatus sql.NullInt32 `json:"http_status"`
Count int32 `json:"count"`
OccurredAt time.Time `json:"occurred_at"`
}
type InstanceMetric struct { type InstanceMetric struct {
ServiceID string `json:"service_id"` ServiceID string `json:"service_id"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
@@ -377,14 +197,6 @@ type InstanceMetric struct {
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
} }
type MetricsTimeseries struct {
ID int32 `json:"id"`
Metric string `json:"metric"`
Value float32 `json:"value"`
LabelsJson sql.NullString `json:"labels_json"`
OccurredAt time.Time `json:"occurred_at"`
}
type NodeAgent struct { type NodeAgent struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -506,16 +318,6 @@ type ProjectMember struct {
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
} }
type ProjectVariable struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Key string `json:"key"`
Value string `json:"value"`
IsSecret bool `json:"is_secret"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Registry struct { type Registry struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"` OwnerID uuid.UUID `json:"owner_id"`
@@ -527,67 +329,52 @@ type Registry struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
type SecurityScan struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.NullUUID `json:"service_id"`
ScanType string `json:"scan_type"`
Status string `json:"status"`
StartedAt time.Time `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
Summary pqtype.NullRawMessage `json:"summary"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Service struct { type Service struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description sql.NullString `json:"description"` Description sql.NullString `json:"description"`
ProjectID uuid.UUID `json:"project_id"` ProjectID uuid.UUID `json:"project_id"`
EnvironmentID uuid.UUID `json:"environment_id"` EnvironmentID uuid.UUID `json:"environment_id"`
ServiceType string `json:"service_type"` ServiceType string `json:"service_type"`
SourceType string `json:"source_type"` SourceType string `json:"source_type"`
SourceUrl sql.NullString `json:"source_url"` SourceUrl sql.NullString `json:"source_url"`
ImageName sql.NullString `json:"image_name"` ImageName sql.NullString `json:"image_name"`
BuildCommand sql.NullString `json:"build_command"` BuildCommand sql.NullString `json:"build_command"`
StartCommand sql.NullString `json:"start_command"` StartCommand sql.NullString `json:"start_command"`
CpuLimit sql.NullInt32 `json:"cpu_limit"` CpuLimit sql.NullInt32 `json:"cpu_limit"`
MemoryLimit sql.NullInt32 `json:"memory_limit"` MemoryLimit sql.NullInt32 `json:"memory_limit"`
PublicUrl sql.NullString `json:"public_url"` PublicUrl sql.NullString `json:"public_url"`
HealthCheckUrl sql.NullString `json:"health_check_url"` HealthCheckUrl sql.NullString `json:"health_check_url"`
Status sql.NullString `json:"status"` Status sql.NullString `json:"status"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
Type sql.NullString `json:"type"` Type sql.NullString `json:"type"`
Image sql.NullString `json:"image"` Image sql.NullString `json:"image"`
Command sql.NullString `json:"command"` Command sql.NullString `json:"command"`
Environment sql.NullString `json:"environment"` Environment sql.NullString `json:"environment"`
GitRepo sql.NullString `json:"git_repo"` GitRepo sql.NullString `json:"git_repo"`
GitBranch sql.NullString `json:"git_branch"` GitBranch sql.NullString `json:"git_branch"`
BuildPath sql.NullString `json:"build_path"` BuildPath sql.NullString `json:"build_path"`
Cpu sql.NullString `json:"cpu"` Cpu sql.NullString `json:"cpu"`
Memory sql.NullString `json:"memory"` Memory sql.NullString `json:"memory"`
Replicas int32 `json:"replicas"` Replicas int32 `json:"replicas"`
Port int32 `json:"port"` Port int32 `json:"port"`
Domain string `json:"domain"` Domain string `json:"domain"`
HealthcheckPath string `json:"healthcheck_path"` HealthcheckPath string `json:"healthcheck_path"`
RestartPolicy string `json:"restart_policy"` RestartPolicy string `json:"restart_policy"`
PublishedPort int32 `json:"published_port"` PublishedPort int32 `json:"published_port"`
Volumes json.RawMessage `json:"volumes"` Volumes json.RawMessage `json:"volumes"`
MaintenanceMode bool `json:"maintenance_mode"` MaintenanceMode bool `json:"maintenance_mode"`
BasicAuthUsers string `json:"basic_auth_users"` BasicAuthUsers string `json:"basic_auth_users"`
Builder string `json:"builder"` Builder string `json:"builder"`
CpuReserve string `json:"cpu_reserve"` CpuReserve string `json:"cpu_reserve"`
MemoryReserve string `json:"memory_reserve"` MemoryReserve string `json:"memory_reserve"`
StaticBuildCmd string `json:"static_build_cmd"` StaticBuildCmd string `json:"static_build_cmd"`
StaticDir string `json:"static_dir"` StaticDir string `json:"static_dir"`
NodeID sql.NullString `json:"node_id"` NodeID sql.NullString `json:"node_id"`
Spread bool `json:"spread"` Spread bool `json:"spread"`
SleepEnabled bool `json:"sleep_enabled"` PlacementTags json.RawMessage `json:"placement_tags"`
SleepIdleMinutes int32 `json:"sleep_idle_minutes"` TraefikLabels json.RawMessage `json:"traefik_labels"`
PlacementTags json.RawMessage `json:"placement_tags"`
TraefikLabels json.RawMessage `json:"traefik_labels"`
} }
type ServiceDomain struct { type ServiceDomain struct {
@@ -643,26 +430,6 @@ type ServiceTemplate struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type UsageCounter struct {
ID int32 `json:"id"`
ApiKeyID uuid.UUID `json:"api_key_id"`
ServiceID uuid.UUID `json:"service_id"`
PeriodMonth string `json:"period_month"`
RequestCount int32 `json:"request_count"`
UpdatedAt time.Time `json:"updated_at"`
}
type User struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
AvatarUrl sql.NullString `json:"avatar_url"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
IsAdmin bool `json:"is_admin"`
}
type UserInvite struct { type UserInvite struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
TokenHash string `json:"token_hash"` TokenHash string `json:"token_hash"`
@@ -687,21 +454,6 @@ type UserToken struct {
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
} }
type Vulnerability struct {
ID uuid.UUID `json:"id"`
Type string `json:"type"`
Severity string `json:"severity"`
Title string `json:"title"`
Description sql.NullString `json:"description"`
ServiceID uuid.NullUUID `json:"service_id"`
ProjectID uuid.UUID `json:"project_id"`
Status string `json:"status"`
FoundAt time.Time `json:"found_at"`
ResolvedAt sql.NullTime `json:"resolved_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WebhookDelivery struct { type WebhookDelivery struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
WebhookID uuid.UUID `json:"webhook_id"` WebhookID uuid.UUID `json:"webhook_id"`
@@ -1,258 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: operations.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const listActiveOperationDeployments = `-- name: ListActiveOperationDeployments :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, d.error, 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 d.status IN ('queued','pending','building','deploying','rolling_back')
AND (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 ASC
`
type ListActiveOperationDeploymentsParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Column2 bool `json:"column_2"`
}
type ListActiveOperationDeploymentsRow 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"`
Image string `json:"image"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) ListActiveOperationDeployments(ctx context.Context, arg ListActiveOperationDeploymentsParams) ([]ListActiveOperationDeploymentsRow, error) {
rows, err := q.db.QueryContext(ctx, listActiveOperationDeployments, arg.OwnerID, arg.Column2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListActiveOperationDeploymentsRow{}
for rows.Next() {
var i ListActiveOperationDeploymentsRow
if err := rows.Scan(
&i.ID,
&i.ServiceID,
&i.ServiceName,
&i.ProjectName,
&i.Status,
&i.Image,
&i.Error,
&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 listRecentCronRuns = `-- name: ListRecentCronRuns :many
SELECT e.id, j.name AS job_name, j.schedule, e.status, e.started_at, e.finished_at, e.error
FROM cron_executions e
JOIN cron_jobs j ON j.id = e.cron_job_id
JOIN projects p ON p.id = j.project_id
WHERE e.started_at > NOW() - INTERVAL '24 hours'
AND (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 e.started_at DESC
LIMIT 30
`
type ListRecentCronRunsParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Column2 bool `json:"column_2"`
}
type ListRecentCronRunsRow struct {
ID uuid.UUID `json:"id"`
JobName string `json:"job_name"`
Schedule string `json:"schedule"`
Status sql.NullString `json:"status"`
StartedAt time.Time `json:"started_at"`
FinishedAt sql.NullTime `json:"finished_at"`
Error sql.NullString `json:"error"`
}
func (q *Queries) ListRecentCronRuns(ctx context.Context, arg ListRecentCronRunsParams) ([]ListRecentCronRunsRow, error) {
rows, err := q.db.QueryContext(ctx, listRecentCronRuns, arg.OwnerID, arg.Column2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListRecentCronRunsRow{}
for rows.Next() {
var i ListRecentCronRunsRow
if err := rows.Scan(
&i.ID,
&i.JobName,
&i.Schedule,
&i.Status,
&i.StartedAt,
&i.FinishedAt,
&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 listRecentFailedDeployments = `-- name: ListRecentFailedDeployments :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, d.error, 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 d.status IN ('failed','cancelled','rolled_back')
AND d.updated_at > NOW() - INTERVAL '24 hours'
AND (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.updated_at DESC
LIMIT 20
`
type ListRecentFailedDeploymentsParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Column2 bool `json:"column_2"`
}
type ListRecentFailedDeploymentsRow 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"`
Image string `json:"image"`
Error sql.NullString `json:"error"`
StartedAt sql.NullTime `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) ListRecentFailedDeployments(ctx context.Context, arg ListRecentFailedDeploymentsParams) ([]ListRecentFailedDeploymentsRow, error) {
rows, err := q.db.QueryContext(ctx, listRecentFailedDeployments, arg.OwnerID, arg.Column2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListRecentFailedDeploymentsRow{}
for rows.Next() {
var i ListRecentFailedDeploymentsRow
if err := rows.Scan(
&i.ID,
&i.ServiceID,
&i.ServiceName,
&i.ProjectName,
&i.Status,
&i.Image,
&i.Error,
&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 listRecentOperationBackups = `-- name: ListRecentOperationBackups :many
SELECT b.id, b.database_id, ds.name AS db_name, b.status, b.size, b.created_at, b.completed_at
FROM database_backups b
JOIN database_services ds ON ds.id = b.database_id
WHERE b.created_at > NOW() - INTERVAL '24 hours'
AND (ds.user_id = $1 OR $2::bool)
ORDER BY b.created_at DESC
LIMIT 20
`
type ListRecentOperationBackupsParams struct {
UserID string `json:"user_id"`
Column2 bool `json:"column_2"`
}
type ListRecentOperationBackupsRow struct {
ID string `json:"id"`
DatabaseID string `json:"database_id"`
DbName string `json:"db_name"`
Status string `json:"status"`
Size string `json:"size"`
CreatedAt sql.NullTime `json:"created_at"`
CompletedAt sql.NullTime `json:"completed_at"`
}
func (q *Queries) ListRecentOperationBackups(ctx context.Context, arg ListRecentOperationBackupsParams) ([]ListRecentOperationBackupsRow, error) {
rows, err := q.db.QueryContext(ctx, listRecentOperationBackups, arg.UserID, arg.Column2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListRecentOperationBackupsRow{}
for rows.Next() {
var i ListRecentOperationBackupsRow
if err := rows.Scan(
&i.ID,
&i.DatabaseID,
&i.DbName,
&i.Status,
&i.Size,
&i.CreatedAt,
&i.CompletedAt,
); 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
}
@@ -16,34 +16,21 @@ type Querier interface {
// Portable bucket bucketing (no TimescaleDB): floor epoch seconds into // Portable bucket bucketing (no TimescaleDB): floor epoch seconds into
// bucket-sized buckets, then back to timestamptz. // bucket-sized buckets, then back to timestamptz.
AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error) AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error)
CancelBuild(ctx context.Context, arg CancelBuildParams) (int64, error)
CheckSecurityProjectAccess(ctx context.Context, arg CheckSecurityProjectAccessParams) (bool, error)
ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error
ClearServiceNodePins(ctx context.Context, nodeID sql.NullString) (int64, error) ClearServiceNodePins(ctx context.Context, nodeID sql.NullString) (int64, error)
CloneServiceDomains(ctx context.Context, arg CloneServiceDomainsParams) error CloneServiceDomains(ctx context.Context, arg CloneServiceDomainsParams) error
CloneServiceRow(ctx context.Context, arg CloneServiceRowParams) error CloneServiceRow(ctx context.Context, arg CloneServiceRowParams) error
CloneServiceVariables(ctx context.Context, arg CloneServiceVariablesParams) error CloneServiceVariables(ctx context.Context, arg CloneServiceVariablesParams) error
CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error) CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error)
CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error
ComplianceFrameworkExists(ctx context.Context, id uuid.UUID) (bool, error)
CountAPKeys(ctx context.Context) (int64, error)
CountAPServices(ctx context.Context) (int64, error)
CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error)
CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error) CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error)
CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error) CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error)
CountDatabasesUsingBackupTarget(ctx context.Context, targetID sql.NullString) (int64, error) CountDatabasesUsingBackupTarget(ctx context.Context, targetID sql.NullString) (int64, error)
CountDeploymentsForService(ctx context.Context, serviceID uuid.UUID) (int64, error)
CountEnvironmentServices(ctx context.Context, environmentID uuid.UUID) (int64, error) CountEnvironmentServices(ctx context.Context, environmentID uuid.UUID) (int64, error)
CountGitRepositories(ctx context.Context, arg CountGitRepositoriesParams) (int64, error)
CountProjects(ctx context.Context) (int64, error)
CountProjectsByUser(ctx context.Context, arg CountProjectsByUserParams) (int64, error) CountProjectsByUser(ctx context.Context, arg CountProjectsByUserParams) (int64, error)
CountServiceDomains(ctx context.Context, serviceID uuid.UUID) (int64, error) CountServiceDomains(ctx context.Context, serviceID uuid.UUID) (int64, error)
CountServicesByName(ctx context.Context, arg CountServicesByNameParams) (int64, error) CountServicesByName(ctx context.Context, arg CountServicesByNameParams) (int64, error)
CountServicesByProjectAndName(ctx context.Context, arg CountServicesByProjectAndNameParams) (int64, error) CountServicesByProjectAndName(ctx context.Context, arg CountServicesByProjectAndNameParams) (int64, error)
CountUnreadNotificationsByUser(ctx context.Context, userID uuid.UUID) (int64, error) CountUnreadNotificationsByUser(ctx context.Context, userID uuid.UUID) (int64, error)
CountUserRepositories(ctx context.Context, userID uuid.UUID) (int64, error)
CountUsers(ctx context.Context) (int64, error)
CountUsersByEmail(ctx context.Context, email string) (int64, error)
CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error) CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error)
CreateAgentAuthToken(ctx context.Context, arg CreateAgentAuthTokenParams) (AgentAuthToken, error) CreateAgentAuthToken(ctx context.Context, arg CreateAgentAuthTokenParams) (AgentAuthToken, error)
CreateBackupTarget(ctx context.Context, arg CreateBackupTargetParams) error CreateBackupTarget(ctx context.Context, arg CreateBackupTargetParams) error
@@ -67,72 +54,34 @@ type Querier interface {
DatabaseServiceExistsByIDAndUser(ctx context.Context, arg DatabaseServiceExistsByIDAndUserParams) (bool, error) DatabaseServiceExistsByIDAndUser(ctx context.Context, arg DatabaseServiceExistsByIDAndUserParams) (bool, error)
DeleteAgent(ctx context.Context, id string) error DeleteAgent(ctx context.Context, id string) error
DeleteAllServiceDomains(ctx context.Context, serviceID uuid.UUID) error DeleteAllServiceDomains(ctx context.Context, serviceID uuid.UUID) error
DeleteAppSetting(ctx context.Context, key string) error
DeleteBackupTargetByIDAndUser(ctx context.Context, arg DeleteBackupTargetByIDAndUserParams) error DeleteBackupTargetByIDAndUser(ctx context.Context, arg DeleteBackupTargetByIDAndUserParams) error
DeleteBanner(ctx context.Context, id uuid.UUID) error DeleteBanner(ctx context.Context, id uuid.UUID) error
DeleteCronJob(ctx context.Context, id uuid.UUID) error
DeleteDatabaseServiceByID(ctx context.Context, id string) error DeleteDatabaseServiceByID(ctx context.Context, id string) error
DeleteDatabaseServiceByIDAndUser(ctx context.Context, arg DeleteDatabaseServiceByIDAndUserParams) error DeleteDatabaseServiceByIDAndUser(ctx context.Context, arg DeleteDatabaseServiceByIDAndUserParams) error
DeleteEnvVarsForService(ctx context.Context, serviceID uuid.UUID) error
DeleteGitProvider(ctx context.Context, arg DeleteGitProviderParams) (int64, error)
DeleteNotificationChannel(ctx context.Context, arg DeleteNotificationChannelParams) (int64, error) DeleteNotificationChannel(ctx context.Context, arg DeleteNotificationChannelParams) (int64, error)
DeleteOutboundWebhookByIDAndUser(ctx context.Context, arg DeleteOutboundWebhookByIDAndUserParams) error DeleteOutboundWebhookByIDAndUser(ctx context.Context, arg DeleteOutboundWebhookByIDAndUserParams) error
DeletePreviewEnvironment(ctx context.Context, id uuid.UUID) error DeletePreviewEnvironment(ctx context.Context, id uuid.UUID) error
DeleteProjectByID(ctx context.Context, projectID uuid.UUID) (int64, error) DeleteProjectByID(ctx context.Context, projectID uuid.UUID) (int64, error)
DeleteProjectEnvironment(ctx context.Context, arg DeleteProjectEnvironmentParams) error DeleteProjectEnvironment(ctx context.Context, arg DeleteProjectEnvironmentParams) error
DeleteProjectVariables(ctx context.Context, projectID uuid.UUID) error
DeleteServiceByID(ctx context.Context, id uuid.UUID) error DeleteServiceByID(ctx context.Context, id uuid.UUID) error
DeleteServiceContainersOnAgent(ctx context.Context, arg DeleteServiceContainersOnAgentParams) error DeleteServiceContainersOnAgent(ctx context.Context, arg DeleteServiceContainersOnAgentParams) error
DeleteServiceDomain(ctx context.Context, arg DeleteServiceDomainParams) (int64, error) DeleteServiceDomain(ctx context.Context, arg DeleteServiceDomainParams) (int64, error)
DeleteUserInvite(ctx context.Context, id uuid.UUID) error DeleteUserInvite(ctx context.Context, id uuid.UUID) error
DeleteUserTemplate(ctx context.Context, arg DeleteUserTemplateParams) (int64, error) DeleteUserTemplate(ctx context.Context, arg DeleteUserTemplateParams) (int64, error)
FailDeployment(ctx context.Context, arg FailDeploymentParams) error
FailInterruptedDeployments(ctx context.Context) error
FinishCronExecution(ctx context.Context, arg FinishCronExecutionParams) error
GetAPServiceBySlug(ctx context.Context, slug string) (GetAPServiceBySlugRow, error)
GetAPServiceForValidate(ctx context.Context, id uuid.UUID) (GetAPServiceForValidateRow, error)
GetActiveAgentAuthTokenByHash(ctx context.Context, tokenHash string) (AgentAuthToken, error) GetActiveAgentAuthTokenByHash(ctx context.Context, tokenHash string) (AgentAuthToken, error)
GetAdminUserInfo(ctx context.Context, id uuid.UUID) (GetAdminUserInfoRow, error)
GetAgent(ctx context.Context, id string) (NodeAgent, error) GetAgent(ctx context.Context, id string) (NodeAgent, error)
GetAgentByHostAndIP(ctx context.Context, arg GetAgentByHostAndIPParams) (NodeAgent, error) GetAgentByHostAndIP(ctx context.Context, arg GetAgentByHostAndIPParams) (NodeAgent, error)
GetAppSetting(ctx context.Context, key string) (string, error)
GetBackupTargetByID(ctx context.Context, id string) (BackupTarget, error) GetBackupTargetByID(ctx context.Context, id string) (BackupTarget, error)
GetBackupTargetByIDAndUser(ctx context.Context, arg GetBackupTargetByIDAndUserParams) (BackupTarget, error) GetBackupTargetByIDAndUser(ctx context.Context, arg GetBackupTargetByIDAndUserParams) (BackupTarget, error)
GetBannerByID(ctx context.Context, id uuid.UUID) (Banner, error) GetBannerByID(ctx context.Context, id uuid.UUID) (Banner, error)
GetBuild(ctx context.Context, id string) (GetBuildRow, error)
GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error) GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error)
GetComplianceReportProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetContainer(ctx context.Context, id string) (ContainerInstance, error) GetContainer(ctx context.Context, id string) (ContainerInstance, error)
GetContainerForAgent(ctx context.Context, arg GetContainerForAgentParams) (ContainerInstance, error) GetContainerForAgent(ctx context.Context, arg GetContainerForAgentParams) (ContainerInstance, error)
GetCronJob(ctx context.Context, id uuid.UUID) (CronJob, error)
GetCronJobForTrigger(ctx context.Context, id uuid.UUID) (GetCronJobForTriggerRow, error)
GetCronJobOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetCronJobProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetCronOwnerViaService(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetDatabaseBackupByIDAndDatabaseAndUser(ctx context.Context, arg GetDatabaseBackupByIDAndDatabaseAndUserParams) (DatabaseBackup, error) GetDatabaseBackupByIDAndDatabaseAndUser(ctx context.Context, arg GetDatabaseBackupByIDAndDatabaseAndUserParams) (DatabaseBackup, error)
GetDatabaseServiceByID(ctx context.Context, id string) (DatabaseService, error) GetDatabaseServiceByID(ctx context.Context, id string) (DatabaseService, error)
GetDatabaseServiceByIDAndUser(ctx context.Context, arg GetDatabaseServiceByIDAndUserParams) (DatabaseService, error) GetDatabaseServiceByIDAndUser(ctx context.Context, arg GetDatabaseServiceByIDAndUserParams) (DatabaseService, error)
GetDatabaseServiceOwnerID(ctx context.Context, id string) (string, error) GetDatabaseServiceOwnerID(ctx context.Context, id string) (string, error)
GetDeploymentAccess(ctx context.Context, id uuid.UUID) (GetDeploymentAccessRow, error)
GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error)
GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error)
GetEnvironmentProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, 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)
GetGitRepoCloneByFullName(ctx context.Context, arg GetGitRepoCloneByFullNameParams) (GetGitRepoCloneByFullNameRow, error)
GetGitRepoForPush(ctx context.Context, id uuid.UUID) (GetGitRepoForPushRow, error)
GetGitRepoProviderByCloneURL(ctx context.Context, arg GetGitRepoProviderByCloneURLParams) (uuid.UUID, error)
GetGitRepositoryByFullName(ctx context.Context, arg GetGitRepositoryByFullNameParams) (GetGitRepositoryByFullNameRow, error)
GetGitRepositoryProviderID(ctx context.Context, arg GetGitRepositoryProviderIDParams) (uuid.UUID, error)
GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error)
GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error) GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error)
GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error)
GetLatestComplianceReport(ctx context.Context, projectID uuid.UUID) (GetLatestComplianceReportRow, error)
GetLatestSecurityScan(ctx context.Context, projectID uuid.UUID) (GetLatestSecurityScanRow, error)
GetNodeAgentName(ctx context.Context, id string) (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)
@@ -146,98 +95,38 @@ type Querier interface {
GetProjectOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error) GetProjectOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetProjectOwnerByID(ctx context.Context, projectID uuid.UUID) (uuid.UUID, error) GetProjectOwnerByID(ctx context.Context, projectID uuid.UUID) (uuid.UUID, error)
GetProjectOwnerID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) GetProjectOwnerID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetProjectOwnerText(ctx context.Context, id uuid.UUID) (string, error)
GetProjectReadAccess(ctx context.Context, id uuid.UUID) (GetProjectReadAccessRow, error)
GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error) GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error)
GetProjectVariableValue(ctx context.Context, arg GetProjectVariableValueParams) (string, error)
GetRegistryAuth(ctx context.Context, arg GetRegistryAuthParams) (GetRegistryAuthRow, error)
GetSecurityScanProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error) GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error)
GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error) GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error)
GetServiceDomainFirst(ctx context.Context, serviceID uuid.UUID) (string, error) GetServiceDomainFirst(ctx context.Context, serviceID uuid.UUID) (string, error)
GetServiceDomainText(ctx context.Context, id uuid.UUID) (string, error)
GetServiceEnvironment(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) GetServiceForDeploy(ctx context.Context, id uuid.UUID) (GetServiceForDeployRow, error)
GetServiceForDeployWithOwner(ctx context.Context, id uuid.UUID) (GetServiceForDeployWithOwnerRow, error)
GetServiceNameProjectPort(ctx context.Context, id uuid.UUID) (GetServiceNameProjectPortRow, error)
GetServiceNodeID(ctx context.Context, id uuid.UUID) (sql.NullString, error)
GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error) GetServicePlacementBrief(ctx context.Context, id uuid.UUID) (GetServicePlacementBriefRow, error)
GetServicePlacementTags(ctx context.Context, id uuid.UUID) (json.RawMessage, error)
GetServicePort(ctx context.Context, id uuid.UUID) (int32, error)
GetServiceProjectAndStatus(ctx context.Context, id uuid.UUID) (GetServiceProjectAndStatusRow, error)
GetServiceProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetServicePublishedPort(ctx context.Context, id uuid.UUID) (int32, error)
GetServiceRuntimeWithOwner(ctx context.Context, id uuid.UUID) (GetServiceRuntimeWithOwnerRow, error)
GetServiceSleep(ctx context.Context, id uuid.UUID) (GetServiceSleepRow, error)
GetServiceSpread(ctx context.Context, id uuid.UUID) (bool, error)
GetServiceStatusText(ctx context.Context, id uuid.UUID) (string, error)
GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error) GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error)
GetServiceTraefikLabels(ctx context.Context, id uuid.UUID) (json.RawMessage, 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)
GetServiceVolumes(ctx context.Context, id uuid.UUID) (json.RawMessage, error)
GetUserByEmailForAuth(ctx context.Context, email string) (GetUserByEmailForAuthRow, error)
GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error)
GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error) GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error)
GetUserIsAdmin(ctx context.Context, id uuid.UUID) (bool, error)
GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error) GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error)
GetVulnerabilityMetrics(ctx context.Context, projectID uuid.UUID) (GetVulnerabilityMetricsRow, error)
GetVulnerabilityProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
InsertAPKey(ctx context.Context, arg InsertAPKeyParams) (uuid.UUID, error)
InsertAPService(ctx context.Context, arg InsertAPServiceParams) (uuid.UUID, error)
InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error
InsertAuditLogWithRequest(ctx context.Context, arg InsertAuditLogWithRequestParams) error
InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error
InsertCronJob(ctx context.Context, arg InsertCronJobParams) error
InsertDeployment(ctx context.Context, arg InsertDeploymentParams) error
InsertEnvVar(ctx context.Context, arg InsertEnvVarParams) error
InsertGatewayIncident(ctx context.Context, arg InsertGatewayIncidentParams) error
InsertGitProvider(ctx context.Context, arg InsertGitProviderParams) error
InsertGitRepository(ctx context.Context, arg InsertGitRepositoryParams) (InsertGitRepositoryRow, error)
InsertMetricPoint(ctx context.Context, labelsJson sql.NullString) error
InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error) InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error)
InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error
InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error
InsertProjectVariable(ctx context.Context, arg InsertProjectVariableParams) error
InsertServiceIncident(ctx context.Context, arg InsertServiceIncidentParams) error
InsertUser(ctx context.Context, arg InsertUserParams) (InsertUserRow, error)
InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams) (InsertUserAdminRow, error)
ListAPKeys(ctx context.Context) ([]ListAPKeysRow, error)
ListAPKeysByPrefix(ctx context.Context, keyPrefix string) ([]ListAPKeysByPrefixRow, error)
ListAPServices(ctx context.Context) ([]ListAPServicesRow, error)
ListActiveBanners(ctx context.Context) ([]Banner, error) ListActiveBanners(ctx context.Context) ([]Banner, error)
ListActiveOperationDeployments(ctx context.Context, arg ListActiveOperationDeploymentsParams) ([]ListActiveOperationDeploymentsRow, error)
ListAdminUserIDs(ctx context.Context) ([]uuid.UUID, error)
ListAdminUsers(ctx context.Context) ([]ListAdminUsersRow, error)
ListAgentAuthTokens(ctx context.Context) ([]AgentAuthToken, error) ListAgentAuthTokens(ctx context.Context) ([]AgentAuthToken, error)
ListAgentHealthRows(ctx context.Context, lastHeartbeat sql.NullTime) ([]ListAgentHealthRowsRow, error)
ListAgentHeartbeatsSince(ctx context.Context, arg ListAgentHeartbeatsSinceParams) ([]AgentHeartbeat, error) ListAgentHeartbeatsSince(ctx context.Context, arg ListAgentHeartbeatsSinceParams) ([]AgentHeartbeat, error)
ListAgents(ctx context.Context) ([]NodeAgent, error) ListAgents(ctx context.Context) ([]NodeAgent, error)
ListAllDatabaseServices(ctx context.Context) ([]DatabaseService, error) ListAllDatabaseServices(ctx context.Context) ([]DatabaseService, error)
ListAllNotificationChannelsByUser(ctx context.Context, userID uuid.UUID) ([]NotificationChannel, error) ListAllNotificationChannelsByUser(ctx context.Context, userID uuid.UUID) ([]NotificationChannel, error)
ListAuditBackfillBatch(ctx context.Context, dollar_1 uuid.UUID) ([]ListAuditBackfillBatchRow, error)
ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error) ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error)
ListBanners(ctx context.Context) ([]Banner, error) ListBanners(ctx context.Context) ([]Banner, error)
ListClientEventPaths(ctx context.Context) ([]ListClientEventPathsRow, error)
ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error) ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error)
ListComplianceFrameworks(ctx context.Context) ([]ListComplianceFrameworksRow, error)
ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error)
ListContainersForAgent(ctx context.Context, nodeAgentID string) ([]ContainerInstance, error) ListContainersForAgent(ctx context.Context, nodeAgentID string) ([]ContainerInstance, error)
ListCronExecutions(ctx context.Context, cronJobID uuid.UUID) ([]CronExecution, error)
ListDatabaseBackupsByDatabaseAndUser(ctx context.Context, arg ListDatabaseBackupsByDatabaseAndUserParams) ([]DatabaseBackup, error) ListDatabaseBackupsByDatabaseAndUser(ctx context.Context, arg ListDatabaseBackupsByDatabaseAndUserParams) ([]DatabaseBackup, error)
ListDatabaseServicesByProject(ctx context.Context, projectID uuid.NullUUID) ([]DatabaseService, error) ListDatabaseServicesByProject(ctx context.Context, projectID uuid.NullUUID) ([]DatabaseService, error)
ListDatabaseServicesByUser(ctx context.Context, userID string) ([]DatabaseService, error) ListDatabaseServicesByUser(ctx context.Context, userID string) ([]DatabaseService, error)
ListDeploymentsForService(ctx context.Context, arg ListDeploymentsForServiceParams) ([]ListDeploymentsForServiceRow, error)
ListDueCronJobs(ctx context.Context) ([]ListDueCronJobsRow, error)
ListDueDatabaseBackups(ctx context.Context) ([]ListDueDatabaseBackupsRow, error) ListDueDatabaseBackups(ctx context.Context) ([]ListDueDatabaseBackupsRow, error)
ListEnabledOutboundWebhooks(ctx context.Context) ([]OutboundWebhook, error) ListEnabledOutboundWebhooks(ctx context.Context) ([]OutboundWebhook, error)
ListExpiredPreviewsForUser(ctx context.Context, arg ListExpiredPreviewsForUserParams) ([]ListExpiredPreviewsForUserRow, error) 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)
ListIncidentStatusCodes(ctx context.Context) ([]ListIncidentStatusCodesRow, 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)
@@ -246,24 +135,16 @@ 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.
ListProjectsWithStatsByUser(ctx context.Context, arg ListProjectsWithStatsByUserParams) ([]ListProjectsWithStatsByUserRow, error) ListProjectsWithStatsByUser(ctx context.Context, arg ListProjectsWithStatsByUserParams) ([]ListProjectsWithStatsByUserRow, error)
ListRecentAccessibleDeployments(ctx context.Context, arg ListRecentAccessibleDeploymentsParams) ([]ListRecentAccessibleDeploymentsRow, error)
ListRecentCronRuns(ctx context.Context, arg ListRecentCronRunsParams) ([]ListRecentCronRunsRow, error)
ListRecentFailedDeployments(ctx context.Context, arg ListRecentFailedDeploymentsParams) ([]ListRecentFailedDeploymentsRow, error)
ListRecentOperationBackups(ctx context.Context, arg ListRecentOperationBackupsParams) ([]ListRecentOperationBackupsRow, error)
ListRequestsByDay(ctx context.Context) ([]ListRequestsByDayRow, error)
ListResourceAuditLogs(ctx context.Context, arg ListResourceAuditLogsParams) ([]ListResourceAuditLogsRow, error)
// Base domains of every online, schedulable agent carrying the required // Base domains of every online, schedulable agent carrying the required
// placement tags — auto-domain candidates for spread services. // placement tags — auto-domain candidates for spread services.
ListSchedulableAgentDomains(ctx context.Context, dollar_1 json.RawMessage) ([]string, error) ListSchedulableAgentDomains(ctx context.Context, dollar_1 json.RawMessage) ([]string, error)
ListSchedulableAgents(ctx context.Context) ([]ListSchedulableAgentsRow, error) ListSchedulableAgents(ctx context.Context) ([]ListSchedulableAgentsRow, error)
// Only agents carrying every required placement tag (jsonb array containment). // Only agents carrying every required placement tag (jsonb array containment).
ListSchedulableAgentsMatching(ctx context.Context, dollar_1 json.RawMessage) ([]ListSchedulableAgentsMatchingRow, error) ListSchedulableAgentsMatching(ctx context.Context, dollar_1 json.RawMessage) ([]ListSchedulableAgentsMatchingRow, error)
ListSecretEnvVars(ctx context.Context) ([]ListSecretEnvVarsRow, error)
// Distinct agents holding inventory rows for a service. // Distinct agents holding inventory rows for a service.
ListServiceAgents(ctx context.Context, serviceID string) ([]string, error) ListServiceAgents(ctx context.Context, serviceID string) ([]string, error)
ListServiceContainers(ctx context.Context, serviceID string) ([]ListServiceContainersRow, error) ListServiceContainers(ctx context.Context, serviceID string) ([]ListServiceContainersRow, error)
@@ -271,15 +152,9 @@ 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)
ListServicesForPush(ctx context.Context, arg ListServicesForPushParams) ([]ListServicesForPushRow, error)
ListSleepCandidates(ctx context.Context) ([]ListSleepCandidatesRow, error)
ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error) ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error)
ListTopAPServices(ctx context.Context) ([]ListTopAPServicesRow, error)
ListUserInvites(ctx context.Context) ([]UserInvite, error) ListUserInvites(ctx context.Context) ([]UserInvite, error)
ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error) ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error)
ListVulnerabilities(ctx context.Context, projectID uuid.UUID) ([]ListVulnerabilitiesRow, error)
ListWebhookDeliveriesByWebhook(ctx context.Context, webhookID uuid.UUID) ([]WebhookDelivery, error) ListWebhookDeliveriesByWebhook(ctx context.Context, webhookID uuid.UUID) ([]WebhookDelivery, error)
MarkAllNotificationsReadByUser(ctx context.Context, arg MarkAllNotificationsReadByUserParams) error MarkAllNotificationsReadByUser(ctx context.Context, arg MarkAllNotificationsReadByUserParams) error
MarkNotificationReadByIDAndUser(ctx context.Context, arg MarkNotificationReadByIDAndUserParams) error MarkNotificationReadByIDAndUser(ctx context.Context, arg MarkNotificationReadByIDAndUserParams) error
@@ -292,21 +167,13 @@ type Querier interface {
// Same resource-aware ordering as PickLeastLoadedAgent, restricted to agents // Same resource-aware ordering as PickLeastLoadedAgent, restricted to agents
// carrying every required placement tag. // carrying every required placement tag.
PickLeastLoadedAgentMatching(ctx context.Context, dollar_1 json.RawMessage) (string, error) PickLeastLoadedAgentMatching(ctx context.Context, dollar_1 json.RawMessage) (string, error)
ProjectMemberExists(ctx context.Context, arg ProjectMemberExistsParams) (bool, error)
PruneAuditLogs(ctx context.Context, dollar_1 int32) (int64, error)
RevokeAgentAuthToken(ctx context.Context, id uuid.UUID) (AgentAuthToken, error) RevokeAgentAuthToken(ctx context.Context, id uuid.UUID) (AgentAuthToken, error)
RevokeUserToken(ctx context.Context, arg RevokeUserTokenParams) (int64, error) RevokeUserToken(ctx context.Context, arg RevokeUserTokenParams) (int64, error)
ScrubCommandPayload(ctx context.Context, arg ScrubCommandPayloadParams) error ScrubCommandPayload(ctx context.Context, arg ScrubCommandPayloadParams) error
ServiceExistsInProject(ctx context.Context, arg ServiceExistsInProjectParams) (bool, error)
ServiceWriteAccess(ctx context.Context, arg ServiceWriteAccessParams) (bool, error) ServiceWriteAccess(ctx context.Context, arg ServiceWriteAccessParams) (bool, error)
SetAPKeyEnabled(ctx context.Context, arg SetAPKeyEnabledParams) error
SetAPServiceEnabled(ctx context.Context, arg SetAPServiceEnabledParams) error
SetAPServiceValidation(ctx context.Context, arg SetAPServiceValidationParams) error
SetAgentDefaultDomain(ctx context.Context, arg SetAgentDefaultDomainParams) error SetAgentDefaultDomain(ctx context.Context, arg SetAgentDefaultDomainParams) error
SetAgentSchedulable(ctx context.Context, arg SetAgentSchedulableParams) error SetAgentSchedulable(ctx context.Context, arg SetAgentSchedulableParams) error
SetAgentStatus(ctx context.Context, arg SetAgentStatusParams) error
SetAgentTags(ctx context.Context, arg SetAgentTagsParams) error SetAgentTags(ctx context.Context, arg SetAgentTagsParams) error
SetAuditLogMetadata(ctx context.Context, arg SetAuditLogMetadataParams) error
SetDatabaseBackupRemoteKeyByID(ctx context.Context, arg SetDatabaseBackupRemoteKeyByIDParams) error SetDatabaseBackupRemoteKeyByID(ctx context.Context, arg SetDatabaseBackupRemoteKeyByIDParams) error
SetDatabaseBackupScheduleByIDAndUser(ctx context.Context, arg SetDatabaseBackupScheduleByIDAndUserParams) error SetDatabaseBackupScheduleByIDAndUser(ctx context.Context, arg SetDatabaseBackupScheduleByIDAndUserParams) error
SetDatabaseBackupStatusByID(ctx context.Context, arg SetDatabaseBackupStatusByIDParams) error SetDatabaseBackupStatusByID(ctx context.Context, arg SetDatabaseBackupStatusByIDParams) error
@@ -316,25 +183,13 @@ type Querier interface {
SetDatabaseServiceStatusAndConnectionByID(ctx context.Context, arg SetDatabaseServiceStatusAndConnectionByIDParams) error SetDatabaseServiceStatusAndConnectionByID(ctx context.Context, arg SetDatabaseServiceStatusAndConnectionByIDParams) error
SetDatabaseServiceStatusByID(ctx context.Context, arg SetDatabaseServiceStatusByIDParams) error SetDatabaseServiceStatusByID(ctx context.Context, arg SetDatabaseServiceStatusByIDParams) error
SetDatabaseServiceStatusByIDAndUser(ctx context.Context, arg SetDatabaseServiceStatusByIDAndUserParams) error SetDatabaseServiceStatusByIDAndUser(ctx context.Context, arg SetDatabaseServiceStatusByIDAndUserParams) error
SetDeploymentStatus(ctx context.Context, arg SetDeploymentStatusParams) error
SetProjectApproved(ctx context.Context, arg SetProjectApprovedParams) (int64, error) SetProjectApproved(ctx context.Context, arg SetProjectApprovedParams) (int64, error)
SetServiceDomainDefault(ctx context.Context, arg SetServiceDomainDefaultParams) (int64, error) SetServiceDomainDefault(ctx context.Context, arg SetServiceDomainDefaultParams) (int64, error)
SetServiceLegacyDomain(ctx context.Context, arg SetServiceLegacyDomainParams) error SetServiceLegacyDomain(ctx context.Context, arg SetServiceLegacyDomainParams) error
SetServicePublishedPort(ctx context.Context, arg SetServicePublishedPortParams) error
SetServiceReplicas(ctx context.Context, arg SetServiceReplicasParams) error
SetServiceStatus(ctx context.Context, arg SetServiceStatusParams) error
SetUserAdmin(ctx context.Context, arg SetUserAdminParams) (int64, error)
SumRequestMetricSince(ctx context.Context, dollar_1 string) (int32, error)
SumUsageByKey(ctx context.Context, arg SumUsageByKeyParams) (int64, error)
SumUsageByService(ctx context.Context, arg SumUsageByServiceParams) (int64, error)
SumUsageCounters(ctx context.Context) (interface{}, error)
SyncDeploymentProgress(ctx context.Context, arg SyncDeploymentProgressParams) error
SyncPreviewStatuses(ctx context.Context) error SyncPreviewStatuses(ctx context.Context) error
TouchAPKeyLastUsed(ctx context.Context, id uuid.UUID) error
TouchAgentAuthToken(ctx context.Context, tokenHash string) error TouchAgentAuthToken(ctx context.Context, tokenHash string) error
TouchServiceDomainChecked(ctx context.Context, id uuid.UUID) error TouchServiceDomainChecked(ctx context.Context, id uuid.UUID) error
TouchUserToken(ctx context.Context, id uuid.UUID) error TouchUserToken(ctx context.Context, id uuid.UUID) error
TrimCronExecutions(ctx context.Context, arg TrimCronExecutionsParams) error
UpdateAgent(ctx context.Context, arg UpdateAgentParams) (NodeAgent, error) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (NodeAgent, error)
UpdateAgentHeartbeat(ctx context.Context, arg UpdateAgentHeartbeatParams) error UpdateAgentHeartbeat(ctx context.Context, arg UpdateAgentHeartbeatParams) error
UpdateBackupTargetByIDAndUser(ctx context.Context, arg UpdateBackupTargetByIDAndUserParams) error UpdateBackupTargetByIDAndUser(ctx context.Context, arg UpdateBackupTargetByIDAndUserParams) error
@@ -347,36 +202,21 @@ type Querier interface {
// removed replica. $4 is the cumulative rx+tx byte counter. // removed replica. $4 is the cumulative rx+tx byte counter.
UpdateContainerStateByName(ctx context.Context, arg UpdateContainerStateByNameParams) error UpdateContainerStateByName(ctx context.Context, arg UpdateContainerStateByNameParams) error
UpdateContainerStatus(ctx context.Context, arg UpdateContainerStatusParams) error UpdateContainerStatus(ctx context.Context, arg UpdateContainerStatusParams) error
UpdateCronJobRun(ctx context.Context, arg UpdateCronJobRunParams) error
UpdateDatabaseServiceNameAndPlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameAndPlanByIDAndUserParams) error UpdateDatabaseServiceNameAndPlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameAndPlanByIDAndUserParams) error
UpdateDatabaseServiceNameByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameByIDAndUserParams) error UpdateDatabaseServiceNameByIDAndUser(ctx context.Context, arg UpdateDatabaseServiceNameByIDAndUserParams) error
UpdateDatabaseServicePlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServicePlanByIDAndUserParams) error UpdateDatabaseServicePlanByIDAndUser(ctx context.Context, arg UpdateDatabaseServicePlanByIDAndUserParams) error
UpdateEnvVarValue(ctx context.Context, arg UpdateEnvVarValueParams) error
UpdateOutboundWebhook(ctx context.Context, arg UpdateOutboundWebhookParams) error UpdateOutboundWebhook(ctx context.Context, arg UpdateOutboundWebhookParams) error
UpdatePreviewEnvironment(ctx context.Context, arg UpdatePreviewEnvironmentParams) error UpdatePreviewEnvironment(ctx context.Context, arg UpdatePreviewEnvironmentParams) error
UpdateProjectByID(ctx context.Context, arg UpdateProjectByIDParams) (int64, error) UpdateProjectByID(ctx context.Context, arg UpdateProjectByIDParams) (int64, error)
UpdateServiceDeployMeta(ctx context.Context, arg UpdateServiceDeployMetaParams) error
UpdateServicePreviewBranch(ctx context.Context, arg UpdateServicePreviewBranchParams) error UpdateServicePreviewBranch(ctx context.Context, arg UpdateServicePreviewBranchParams) error
UpdateServiceRuntimeMeta(ctx context.Context, arg UpdateServiceRuntimeMetaParams) error
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error) UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error)
UpdateVulnerabilityStatus(ctx context.Context, arg UpdateVulnerabilityStatusParams) error
UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error
UpsertAdminUser(ctx context.Context, arg UpsertAdminUserParams) (uuid.UUID, error)
UpsertAppSetting(ctx context.Context, arg UpsertAppSettingParams) error
UpsertAppSettingValue(ctx context.Context, arg UpsertAppSettingValueParams) error
UpsertBuild(ctx context.Context, arg UpsertBuildParams) error
UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error
UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error)
UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error
UpsertGitWebhook(ctx context.Context, arg UpsertGitWebhookParams) (UpsertGitWebhookRow, error)
UpsertInstanceMetrics(ctx context.Context, arg UpsertInstanceMetricsParams) error UpsertInstanceMetrics(ctx context.Context, arg UpsertInstanceMetricsParams) error
UpsertLocalUser(ctx context.Context, arg UpsertLocalUserParams) (UpsertLocalUserRow, error)
UpsertNodeMetrics(ctx context.Context, arg UpsertNodeMetricsParams) error UpsertNodeMetrics(ctx context.Context, arg UpsertNodeMetricsParams) error
UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error
UpsertServiceMetrics(ctx context.Context, arg UpsertServiceMetricsParams) error UpsertServiceMetrics(ctx context.Context, arg UpsertServiceMetricsParams) error
UpsertServiceTemplate(ctx context.Context, arg UpsertServiceTemplateParams) error UpsertServiceTemplate(ctx context.Context, arg UpsertServiceTemplateParams) error
UpsertUsageCounter(ctx context.Context, arg UpsertUsageCounterParams) error
} }
var _ Querier = (*Queries)(nil) var _ Querier = (*Queries)(nil)
@@ -1,355 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: runtime.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const getLastDeployedImage = `-- name: GetLastDeployedImage :one
SELECT (image_name || ':' || image_tag)::text AS image FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1
`
func (q *Queries) GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getLastDeployedImage, serviceID)
var image string
err := row.Scan(&image)
return image, err
}
const getServiceDomainText = `-- name: GetServiceDomainText :one
SELECT COALESCE(domain,'') AS domain FROM services WHERE id = $1
`
func (q *Queries) GetServiceDomainText(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceDomainText, id)
var domain string
err := row.Scan(&domain)
return domain, err
}
const getServicePort = `-- name: GetServicePort :one
SELECT COALESCE(port, 0) AS port FROM services WHERE id = $1
`
func (q *Queries) GetServicePort(ctx context.Context, id uuid.UUID) (int32, error) {
row := q.db.QueryRowContext(ctx, getServicePort, id)
var port int32
err := row.Scan(&port)
return port, err
}
const getServiceRuntimeWithOwner = `-- name: GetServiceRuntimeWithOwner :one
SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status,
COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command,
COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo,
COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path,
COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS memory,
COALESCE(s.replicas, 1) AS replicas, COALESCE(s.port, 0) AS port,
COALESCE(s.domain, '') AS domain, COALESCE(s.healthcheck_path, '') AS healthcheck_path,
COALESCE(s.restart_policy, 'unless-stopped') AS restart_policy,
COALESCE(s.builder, 'auto') AS builder, COALESCE(s.cpu_reserve, '') AS cpu_reserve,
COALESCE(s.memory_reserve, '') AS memory_reserve, COALESCE(s.static_build_cmd, '') AS static_build_cmd,
COALESCE(s.static_dir, '') AS static_dir,
s.created_at, s.updated_at, p.owner_id
FROM services s
JOIN projects p ON s.project_id = p.id
WHERE s.id = $1
`
type GetServiceRuntimeWithOwnerRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
Image string `json:"image"`
Command string `json:"command"`
Environment string `json:"environment"`
GitRepo string `json:"git_repo"`
GitBranch string `json:"git_branch"`
BuildPath string `json:"build_path"`
Cpu string `json:"cpu"`
Memory string `json:"memory"`
Replicas int32 `json:"replicas"`
Port int32 `json:"port"`
Domain string `json:"domain"`
HealthcheckPath string `json:"healthcheck_path"`
RestartPolicy string `json:"restart_policy"`
Builder string `json:"builder"`
CpuReserve string `json:"cpu_reserve"`
MemoryReserve string `json:"memory_reserve"`
StaticBuildCmd string `json:"static_build_cmd"`
StaticDir string `json:"static_dir"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) GetServiceRuntimeWithOwner(ctx context.Context, id uuid.UUID) (GetServiceRuntimeWithOwnerRow, error) {
row := q.db.QueryRowContext(ctx, getServiceRuntimeWithOwner, id)
var i GetServiceRuntimeWithOwnerRow
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Type,
&i.Status,
&i.Image,
&i.Command,
&i.Environment,
&i.GitRepo,
&i.GitBranch,
&i.BuildPath,
&i.Cpu,
&i.Memory,
&i.Replicas,
&i.Port,
&i.Domain,
&i.HealthcheckPath,
&i.RestartPolicy,
&i.Builder,
&i.CpuReserve,
&i.MemoryReserve,
&i.StaticBuildCmd,
&i.StaticDir,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
const getServiceStatusText = `-- name: GetServiceStatusText :one
SELECT COALESCE(status,'') AS status FROM services WHERE id = $1
`
func (q *Queries) GetServiceStatusText(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceStatusText, id)
var status string
err := row.Scan(&status)
return status, err
}
const getServiceVariableValue = `-- name: GetServiceVariableValue :one
SELECT value FROM environment_variables WHERE service_id = $1 AND key = $2
`
type GetServiceVariableValueParams struct {
ServiceID uuid.UUID `json:"service_id"`
Key string `json:"key"`
}
func (q *Queries) GetServiceVariableValue(ctx context.Context, arg GetServiceVariableValueParams) (string, error) {
row := q.db.QueryRowContext(ctx, getServiceVariableValue, arg.ServiceID, arg.Key)
var value string
err := row.Scan(&value)
return value, err
}
const listNodeAgentsForService = `-- name: ListNodeAgentsForService :many
SELECT DISTINCT node_agent_id FROM container_instances WHERE service_id = $1
`
func (q *Queries) ListNodeAgentsForService(ctx context.Context, serviceID string) ([]string, error) {
rows, err := q.db.QueryContext(ctx, listNodeAgentsForService, serviceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []string{}
for rows.Next() {
var node_agent_id string
if err := rows.Scan(&node_agent_id); err != nil {
return nil, err
}
items = append(items, node_agent_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listProjectServiceRefs = `-- name: ListProjectServiceRefs :many
SELECT id, name FROM services WHERE project_id = $1
`
type ListProjectServiceRefsRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
}
func (q *Queries) ListProjectServiceRefs(ctx context.Context, projectID uuid.UUID) ([]ListProjectServiceRefsRow, error) {
rows, err := q.db.QueryContext(ctx, listProjectServiceRefs, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListProjectServiceRefsRow{}
for rows.Next() {
var i ListProjectServiceRefsRow
if err := rows.Scan(&i.ID, &i.Name); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listServiceVariableValues = `-- name: ListServiceVariableValues :many
SELECT key, value FROM environment_variables WHERE service_id = $1
`
type ListServiceVariableValuesRow struct {
Key string `json:"key"`
Value string `json:"value"`
}
func (q *Queries) ListServiceVariableValues(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariableValuesRow, error) {
rows, err := q.db.QueryContext(ctx, listServiceVariableValues, serviceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListServiceVariableValuesRow{}
for rows.Next() {
var i ListServiceVariableValuesRow
if err := rows.Scan(&i.Key, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listServiceVariablesWithSecret = `-- name: ListServiceVariablesWithSecret :many
SELECT key, value, COALESCE(is_secret, false) AS is_secret
FROM environment_variables WHERE service_id = $1
`
type ListServiceVariablesWithSecretRow struct {
Key string `json:"key"`
Value string `json:"value"`
IsSecret bool `json:"is_secret"`
}
func (q *Queries) ListServiceVariablesWithSecret(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariablesWithSecretRow, error) {
rows, err := q.db.QueryContext(ctx, listServiceVariablesWithSecret, serviceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListServiceVariablesWithSecretRow{}
for rows.Next() {
var i ListServiceVariablesWithSecretRow
if err := rows.Scan(&i.Key, &i.Value, &i.IsSecret); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listSleepCandidates = `-- name: ListSleepCandidates :many
SELECT id, project_id, name, COALESCE(status,'') AS status,
COALESCE(sleep_idle_minutes, 15) AS sleep_idle_minutes
FROM services
WHERE sleep_enabled AND COALESCE(status,'') IN ('running','deployed','degraded','starting')
`
type ListSleepCandidatesRow struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
Name string `json:"name"`
Status string `json:"status"`
SleepIdleMinutes int32 `json:"sleep_idle_minutes"`
}
func (q *Queries) ListSleepCandidates(ctx context.Context) ([]ListSleepCandidatesRow, error) {
rows, err := q.db.QueryContext(ctx, listSleepCandidates)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListSleepCandidatesRow{}
for rows.Next() {
var i ListSleepCandidatesRow
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Status,
&i.SleepIdleMinutes,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setServicePublishedPort = `-- name: SetServicePublishedPort :exec
UPDATE services SET published_port = $1 WHERE id = $2 AND published_port <> $1
`
type SetServicePublishedPortParams struct {
PublishedPort int32 `json:"published_port"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetServicePublishedPort(ctx context.Context, arg SetServicePublishedPortParams) error {
_, err := q.db.ExecContext(ctx, setServicePublishedPort, arg.PublishedPort, arg.ID)
return err
}
const setServiceReplicas = `-- name: SetServiceReplicas :exec
UPDATE services SET replicas = $1, updated_at = $2 WHERE id = $3
`
type SetServiceReplicasParams struct {
Replicas int32 `json:"replicas"`
UpdatedAt sql.NullTime `json:"updated_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) SetServiceReplicas(ctx context.Context, arg SetServiceReplicasParams) error {
_, err := q.db.ExecContext(ctx, setServiceReplicas, arg.Replicas, arg.UpdatedAt, arg.ID)
return err
}
@@ -1,318 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: security.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const checkSecurityProjectAccess = `-- name: CheckSecurityProjectAccess :one
SELECT EXISTS (
SELECT 1
FROM projects p
WHERE p.id = $1
AND (p.is_approved
OR $3::bool
OR p.owner_id = $2 OR EXISTS (
SELECT 1 FROM project_members pm
WHERE pm.project_id = p.id AND pm.user_id = $2
))
)
`
type CheckSecurityProjectAccessParams struct {
ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"`
Column3 bool `json:"column_3"`
}
func (q *Queries) CheckSecurityProjectAccess(ctx context.Context, arg CheckSecurityProjectAccessParams) (bool, error) {
row := q.db.QueryRowContext(ctx, checkSecurityProjectAccess, arg.ID, arg.OwnerID, arg.Column3)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const complianceFrameworkExists = `-- name: ComplianceFrameworkExists :one
SELECT EXISTS(SELECT 1 FROM compliance_frameworks WHERE id = $1)
`
func (q *Queries) ComplianceFrameworkExists(ctx context.Context, id uuid.UUID) (bool, error) {
row := q.db.QueryRowContext(ctx, complianceFrameworkExists, id)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const getComplianceReportProjectID = `-- name: GetComplianceReportProjectID :one
SELECT project_id FROM compliance_reports WHERE id = $1
`
func (q *Queries) GetComplianceReportProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getComplianceReportProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getLatestComplianceReport = `-- name: GetLatestComplianceReport :one
SELECT overall_status, score, assessment_date
FROM compliance_reports
WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1
`
type GetLatestComplianceReportRow struct {
OverallStatus string `json:"overall_status"`
Score int32 `json:"score"`
AssessmentDate time.Time `json:"assessment_date"`
}
func (q *Queries) GetLatestComplianceReport(ctx context.Context, projectID uuid.UUID) (GetLatestComplianceReportRow, error) {
row := q.db.QueryRowContext(ctx, getLatestComplianceReport, projectID)
var i GetLatestComplianceReportRow
err := row.Scan(&i.OverallStatus, &i.Score, &i.AssessmentDate)
return i, err
}
const getLatestSecurityScan = `-- name: GetLatestSecurityScan :one
SELECT id, COALESCE((summary->>'score')::int, 0)::int AS score, started_at AS scanned_at, status
FROM security_scans
WHERE project_id = $1
ORDER BY started_at DESC
LIMIT 1
`
type GetLatestSecurityScanRow struct {
ID uuid.UUID `json:"id"`
Score int32 `json:"score"`
ScannedAt time.Time `json:"scanned_at"`
Status string `json:"status"`
}
func (q *Queries) GetLatestSecurityScan(ctx context.Context, projectID uuid.UUID) (GetLatestSecurityScanRow, error) {
row := q.db.QueryRowContext(ctx, getLatestSecurityScan, projectID)
var i GetLatestSecurityScanRow
err := row.Scan(
&i.ID,
&i.Score,
&i.ScannedAt,
&i.Status,
)
return i, err
}
const getProjectOwnerText = `-- name: GetProjectOwnerText :one
SELECT owner_id::text FROM projects WHERE id = $1
`
func (q *Queries) GetProjectOwnerText(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getProjectOwnerText, id)
var owner_id string
err := row.Scan(&owner_id)
return owner_id, err
}
const getSecurityScanProjectID = `-- name: GetSecurityScanProjectID :one
SELECT project_id FROM security_scans WHERE id = $1
`
func (q *Queries) GetSecurityScanProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getSecurityScanProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getVulnerabilityMetrics = `-- name: GetVulnerabilityMetrics :one
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE severity = 'critical') AS critical,
COUNT(*) FILTER (WHERE severity = 'high') AS high,
COUNT(*) FILTER (WHERE severity = 'medium') AS medium,
COUNT(*) FILTER (WHERE severity = 'low') AS low,
COUNT(*) FILTER (WHERE status = 'open') AS open,
COUNT(*) FILTER (WHERE status = 'resolved') AS resolved
FROM vulnerabilities
WHERE project_id = $1
`
type GetVulnerabilityMetricsRow struct {
Total int64 `json:"total"`
Critical int64 `json:"critical"`
High int64 `json:"high"`
Medium int64 `json:"medium"`
Low int64 `json:"low"`
Open int64 `json:"open"`
Resolved int64 `json:"resolved"`
}
func (q *Queries) GetVulnerabilityMetrics(ctx context.Context, projectID uuid.UUID) (GetVulnerabilityMetricsRow, error) {
row := q.db.QueryRowContext(ctx, getVulnerabilityMetrics, projectID)
var i GetVulnerabilityMetricsRow
err := row.Scan(
&i.Total,
&i.Critical,
&i.High,
&i.Medium,
&i.Low,
&i.Open,
&i.Resolved,
)
return i, err
}
const getVulnerabilityProjectID = `-- name: GetVulnerabilityProjectID :one
SELECT project_id FROM vulnerabilities WHERE id = $1
`
func (q *Queries) GetVulnerabilityProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getVulnerabilityProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const listComplianceFrameworks = `-- name: ListComplianceFrameworks :many
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name
`
type ListComplianceFrameworksRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description sql.NullString `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) ListComplianceFrameworks(ctx context.Context) ([]ListComplianceFrameworksRow, error) {
rows, err := q.db.QueryContext(ctx, listComplianceFrameworks)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListComplianceFrameworksRow{}
for rows.Next() {
var i ListComplianceFrameworksRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Version,
&i.Enabled,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listVulnerabilities = `-- name: ListVulnerabilities :many
SELECT id, type, severity, title, description, service_id, status, found_at, resolved_at
FROM vulnerabilities
WHERE project_id = $1
ORDER BY
CASE severity
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
found_at DESC
`
type ListVulnerabilitiesRow struct {
ID uuid.UUID `json:"id"`
Type string `json:"type"`
Severity string `json:"severity"`
Title string `json:"title"`
Description sql.NullString `json:"description"`
ServiceID uuid.NullUUID `json:"service_id"`
Status string `json:"status"`
FoundAt time.Time `json:"found_at"`
ResolvedAt sql.NullTime `json:"resolved_at"`
}
func (q *Queries) ListVulnerabilities(ctx context.Context, projectID uuid.UUID) ([]ListVulnerabilitiesRow, error) {
rows, err := q.db.QueryContext(ctx, listVulnerabilities, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListVulnerabilitiesRow{}
for rows.Next() {
var i ListVulnerabilitiesRow
if err := rows.Scan(
&i.ID,
&i.Type,
&i.Severity,
&i.Title,
&i.Description,
&i.ServiceID,
&i.Status,
&i.FoundAt,
&i.ResolvedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const serviceExistsInProject = `-- name: ServiceExistsInProject :one
SELECT EXISTS(SELECT 1 FROM services WHERE id = $1 AND project_id = $2)
`
type ServiceExistsInProjectParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
}
func (q *Queries) ServiceExistsInProject(ctx context.Context, arg ServiceExistsInProjectParams) (bool, error) {
row := q.db.QueryRowContext(ctx, serviceExistsInProject, arg.ID, arg.ProjectID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const updateVulnerabilityStatus = `-- name: UpdateVulnerabilityStatus :exec
UPDATE vulnerabilities SET status = $1, resolved_at = $2 WHERE id = $3
`
type UpdateVulnerabilityStatusParams struct {
Status string `json:"status"`
ResolvedAt sql.NullTime `json:"resolved_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateVulnerabilityStatus(ctx context.Context, arg UpdateVulnerabilityStatusParams) error {
_, err := q.db.ExecContext(ctx, updateVulnerabilityStatus, arg.Status, arg.ResolvedAt, arg.ID)
return err
}
@@ -1,308 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: services_meta.sql
package sqlcdb
import (
"context"
"database/sql"
"encoding/json"
"github.com/google/uuid"
)
const deleteEnvVarsForService = `-- name: DeleteEnvVarsForService :exec
DELETE FROM environment_variables WHERE service_id = $1
`
func (q *Queries) DeleteEnvVarsForService(ctx context.Context, serviceID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteEnvVarsForService, serviceID)
return err
}
const failInterruptedDeployments = `-- name: FailInterruptedDeployments :exec
UPDATE deployments SET status = 'failed', error = 'Interrupted by server restart', completed_at = NOW(), updated_at = NOW()
WHERE status IN ('queued', 'pending', 'building', 'deploying', 'rolling_back')
`
func (q *Queries) FailInterruptedDeployments(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, failInterruptedDeployments)
return err
}
const getEnvironmentProjectID = `-- name: GetEnvironmentProjectID :one
SELECT project_id FROM environments WHERE id = $1
`
func (q *Queries) GetEnvironmentProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getEnvironmentProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getNodeAgentName = `-- name: GetNodeAgentName :one
SELECT name FROM node_agents WHERE id = $1
`
func (q *Queries) GetNodeAgentName(ctx context.Context, id string) (string, error) {
row := q.db.QueryRowContext(ctx, getNodeAgentName, id)
var name string
err := row.Scan(&name)
return name, err
}
const getProjectReadAccess = `-- name: GetProjectReadAccess :one
SELECT is_approved, owner_id::text FROM projects WHERE id = $1
`
type GetProjectReadAccessRow struct {
IsApproved bool `json:"is_approved"`
OwnerID string `json:"owner_id"`
}
func (q *Queries) GetProjectReadAccess(ctx context.Context, id uuid.UUID) (GetProjectReadAccessRow, error) {
row := q.db.QueryRowContext(ctx, getProjectReadAccess, id)
var i GetProjectReadAccessRow
err := row.Scan(&i.IsApproved, &i.OwnerID)
return i, err
}
const getServiceNameProjectPort = `-- name: GetServiceNameProjectPort :one
SELECT name, project_id::text, COALESCE(port, 0)::int AS port
FROM services WHERE id::text = $1
`
type GetServiceNameProjectPortRow struct {
Name string `json:"name"`
ProjectID string `json:"project_id"`
Port int32 `json:"port"`
}
func (q *Queries) GetServiceNameProjectPort(ctx context.Context, id uuid.UUID) (GetServiceNameProjectPortRow, error) {
row := q.db.QueryRowContext(ctx, getServiceNameProjectPort, id)
var i GetServiceNameProjectPortRow
err := row.Scan(&i.Name, &i.ProjectID, &i.Port)
return i, err
}
const getServiceNodeID = `-- name: GetServiceNodeID :one
SELECT node_id FROM services WHERE id = $1
`
func (q *Queries) GetServiceNodeID(ctx context.Context, id uuid.UUID) (sql.NullString, error) {
row := q.db.QueryRowContext(ctx, getServiceNodeID, id)
var node_id sql.NullString
err := row.Scan(&node_id)
return node_id, err
}
const getServicePlacementTags = `-- name: GetServicePlacementTags :one
SELECT placement_tags FROM services WHERE id = $1
`
func (q *Queries) GetServicePlacementTags(ctx context.Context, id uuid.UUID) (json.RawMessage, error) {
row := q.db.QueryRowContext(ctx, getServicePlacementTags, id)
var placement_tags json.RawMessage
err := row.Scan(&placement_tags)
return placement_tags, err
}
const getServiceProjectAndStatus = `-- name: GetServiceProjectAndStatus :one
SELECT s.project_id, COALESCE(s.status, '') AS status
FROM services s WHERE s.id = $1
`
type GetServiceProjectAndStatusRow struct {
ProjectID uuid.UUID `json:"project_id"`
Status string `json:"status"`
}
func (q *Queries) GetServiceProjectAndStatus(ctx context.Context, id uuid.UUID) (GetServiceProjectAndStatusRow, error) {
row := q.db.QueryRowContext(ctx, getServiceProjectAndStatus, id)
var i GetServiceProjectAndStatusRow
err := row.Scan(&i.ProjectID, &i.Status)
return i, err
}
const getServiceProjectID = `-- name: GetServiceProjectID :one
SELECT project_id FROM services WHERE id = $1
`
func (q *Queries) GetServiceProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getServiceProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getServiceSleep = `-- name: GetServiceSleep :one
SELECT COALESCE(sleep_enabled, false) AS sleep_enabled, COALESCE(sleep_idle_minutes, 15) AS sleep_idle_minutes
FROM services WHERE id = $1
`
type GetServiceSleepRow struct {
SleepEnabled bool `json:"sleep_enabled"`
SleepIdleMinutes int32 `json:"sleep_idle_minutes"`
}
func (q *Queries) GetServiceSleep(ctx context.Context, id uuid.UUID) (GetServiceSleepRow, error) {
row := q.db.QueryRowContext(ctx, getServiceSleep, id)
var i GetServiceSleepRow
err := row.Scan(&i.SleepEnabled, &i.SleepIdleMinutes)
return i, err
}
const getServiceSpread = `-- name: GetServiceSpread :one
SELECT COALESCE(spread, false) FROM services WHERE id = $1
`
func (q *Queries) GetServiceSpread(ctx context.Context, id uuid.UUID) (bool, error) {
row := q.db.QueryRowContext(ctx, getServiceSpread, id)
var spread bool
err := row.Scan(&spread)
return spread, err
}
const getServiceTraefikLabels = `-- name: GetServiceTraefikLabels :one
SELECT traefik_labels FROM services WHERE id = $1
`
func (q *Queries) GetServiceTraefikLabels(ctx context.Context, id uuid.UUID) (json.RawMessage, error) {
row := q.db.QueryRowContext(ctx, getServiceTraefikLabels, id)
var traefik_labels json.RawMessage
err := row.Scan(&traefik_labels)
return traefik_labels, err
}
const getServiceVolumes = `-- name: GetServiceVolumes :one
SELECT volumes FROM services WHERE id = $1
`
func (q *Queries) GetServiceVolumes(ctx context.Context, id uuid.UUID) (json.RawMessage, error) {
row := q.db.QueryRowContext(ctx, getServiceVolumes, id)
var volumes json.RawMessage
err := row.Scan(&volumes)
return volumes, err
}
const listAgentHealthRows = `-- name: ListAgentHealthRows :many
SELECT id, COALESCE(name, hostname, id::text) AS name, COALESCE(status, '') AS status,
COALESCE(last_heartbeat < $1, TRUE)::bool AS stale, auto_prune
FROM node_agents
`
type ListAgentHealthRowsRow struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Stale bool `json:"stale"`
AutoPrune bool `json:"auto_prune"`
}
func (q *Queries) ListAgentHealthRows(ctx context.Context, lastHeartbeat sql.NullTime) ([]ListAgentHealthRowsRow, error) {
rows, err := q.db.QueryContext(ctx, listAgentHealthRows, lastHeartbeat)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListAgentHealthRowsRow{}
for rows.Next() {
var i ListAgentHealthRowsRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Status,
&i.Stale,
&i.AutoPrune,
); 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 listSecretEnvVars = `-- name: ListSecretEnvVars :many
SELECT id, value FROM environment_variables WHERE is_secret = TRUE
`
type ListSecretEnvVarsRow struct {
ID uuid.UUID `json:"id"`
Value string `json:"value"`
}
func (q *Queries) ListSecretEnvVars(ctx context.Context) ([]ListSecretEnvVarsRow, error) {
rows, err := q.db.QueryContext(ctx, listSecretEnvVars)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListSecretEnvVarsRow{}
for rows.Next() {
var i ListSecretEnvVarsRow
if err := rows.Scan(&i.ID, &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 projectMemberExists = `-- name: ProjectMemberExists :one
SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2)
`
type ProjectMemberExistsParams struct {
ProjectID uuid.UUID `json:"project_id"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) ProjectMemberExists(ctx context.Context, arg ProjectMemberExistsParams) (bool, error) {
row := q.db.QueryRowContext(ctx, projectMemberExists, arg.ProjectID, arg.UserID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const setAgentStatus = `-- name: SetAgentStatus :exec
UPDATE node_agents SET status = $1, updated_at = NOW() WHERE id = $2
`
type SetAgentStatusParams struct {
Status sql.NullString `json:"status"`
ID string `json:"id"`
}
func (q *Queries) SetAgentStatus(ctx context.Context, arg SetAgentStatusParams) error {
_, err := q.db.ExecContext(ctx, setAgentStatus, arg.Status, arg.ID)
return err
}
const updateEnvVarValue = `-- name: UpdateEnvVarValue :exec
UPDATE environment_variables SET value = $1, updated_at = NOW() WHERE id = $2
`
type UpdateEnvVarValueParams struct {
Value string `json:"value"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateEnvVarValue(ctx context.Context, arg UpdateEnvVarValueParams) error {
_, err := q.db.ExecContext(ctx, updateEnvVarValue, arg.Value, arg.ID)
return err
}
@@ -1,103 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: settings.sql
package sqlcdb
import (
"context"
"github.com/google/uuid"
)
const deleteAppSetting = `-- name: DeleteAppSetting :exec
DELETE FROM app_settings WHERE key = $1
`
func (q *Queries) DeleteAppSetting(ctx context.Context, key string) error {
_, err := q.db.ExecContext(ctx, deleteAppSetting, key)
return err
}
const getAppSetting = `-- name: GetAppSetting :one
SELECT value FROM app_settings WHERE key = $1
`
func (q *Queries) GetAppSetting(ctx context.Context, key string) (string, error) {
row := q.db.QueryRowContext(ctx, getAppSetting, key)
var value string
err := row.Scan(&value)
return value, err
}
const getUserIsAdmin = `-- name: GetUserIsAdmin :one
SELECT is_admin FROM users WHERE id = $1
`
func (q *Queries) GetUserIsAdmin(ctx context.Context, id uuid.UUID) (bool, error) {
row := q.db.QueryRowContext(ctx, getUserIsAdmin, id)
var is_admin bool
err := row.Scan(&is_admin)
return is_admin, err
}
const listAdminUserIDs = `-- name: ListAdminUserIDs :many
SELECT id FROM users WHERE is_admin
`
func (q *Queries) ListAdminUserIDs(ctx context.Context) ([]uuid.UUID, error) {
rows, err := q.db.QueryContext(ctx, listAdminUserIDs)
if err != nil {
return nil, err
}
defer rows.Close()
items := []uuid.UUID{}
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertAppSetting = `-- name: UpsertAppSetting :exec
INSERT INTO app_settings (key, value, is_secret, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value, is_secret = EXCLUDED.is_secret, updated_at = NOW()
`
type UpsertAppSettingParams struct {
Key string `json:"key"`
Value string `json:"value"`
IsSecret bool `json:"is_secret"`
}
func (q *Queries) UpsertAppSetting(ctx context.Context, arg UpsertAppSettingParams) error {
_, err := q.db.ExecContext(ctx, upsertAppSetting, arg.Key, arg.Value, arg.IsSecret)
return err
}
const upsertAppSettingValue = `-- name: UpsertAppSettingValue :exec
INSERT INTO app_settings (key, value, updated_at) VALUES ($1, $2, NOW())
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()
`
type UpsertAppSettingValueParams struct {
Key string `json:"key"`
Value string `json:"value"`
}
func (q *Queries) UpsertAppSettingValue(ctx context.Context, arg UpsertAppSettingValueParams) error {
_, err := q.db.ExecContext(ctx, upsertAppSettingValue, arg.Key, arg.Value)
return err
}
-125
View File
@@ -1,125 +0,0 @@
-- name: GetAPServiceForValidate :one
SELECT upstream_url, health_path, upstream_auth_header, upstream_auth_value, request_timeout_ms
FROM api_services WHERE id = $1;
-- name: SetAPServiceValidation :exec
UPDATE api_services
SET last_validation_at = NOW(), last_validation_status = $1, last_validation_message = $2, updated_at = NOW()
WHERE id = $3;
-- name: InsertServiceIncident :exec
INSERT INTO incident_events (service_id, code, message, severity, http_status, occurred_at)
VALUES ($1, 'SERVICE_VALIDATION_FAILED', $2, 'medium', $3, NOW());
-- name: InsertGatewayIncident :exec
INSERT INTO incident_events (service_id, api_key_id, code, message, severity, http_status, occurred_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW());
-- name: ListAPServices :many
SELECT id, name, slug, upstream_url, route_prefix, enabled,
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
to_char(updated_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS updated_at
FROM api_services
ORDER BY created_at DESC;
-- name: InsertAPService :one
INSERT INTO api_services (
name, slug, upstream_url, route_prefix, health_path,
rpm_limit, monthly_quota, created_at, updated_at
) VALUES ($1, $2, $3, $4, '/health', $5, $6, NOW(), NOW())
RETURNING id;
-- name: SetAPServiceEnabled :exec
UPDATE api_services SET enabled = $1, updated_at = NOW() WHERE id = $2;
-- name: ListAPKeys :many
SELECT id, name, key_prefix, plan, enabled, rpm_limit, monthly_quota,
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
to_char(updated_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS updated_at
FROM api_keys
ORDER BY created_at DESC;
-- name: InsertAPKey :one
INSERT INTO api_keys (
name, key_hash, key_prefix, plan, allowed_service_ids,
rpm_limit, monthly_quota, created_at, updated_at
) VALUES ($1, $2, $3, $4, '[]', $5, $6, NOW(), NOW())
RETURNING id;
-- name: SetAPKeyEnabled :exec
UPDATE api_keys SET enabled = $1, updated_at = NOW() WHERE id = $2;
-- name: CountAPServices :one
SELECT COUNT(*) FROM api_services;
-- name: CountAPKeys :one
SELECT COUNT(*) FROM api_keys;
-- name: SumUsageCounters :one
SELECT COALESCE(SUM(request_count), 0) FROM usage_counters;
-- name: SumRequestMetricSince :one
SELECT COALESCE(SUM(value), 0)::int
FROM metrics_timeseries
WHERE metric = 'request_total' AND occurred_at >= DATE_TRUNC($1::text, NOW());
-- name: ListTopAPServices :many
SELECT s.id, s.name, COALESCE(SUM(u.request_count), 0) AS total_requests
FROM api_services s
LEFT JOIN usage_counters u ON u.service_id = s.id
GROUP BY s.id, s.name
ORDER BY total_requests DESC, s.name ASC
LIMIT 10;
-- name: ListRequestsByDay :many
SELECT TO_CHAR(DATE_TRUNC('day', occurred_at), 'YYYY-MM-DD') AS day_bucket, COUNT(*) AS total
FROM metrics_timeseries
WHERE metric = 'request_total' AND occurred_at >= NOW() - INTERVAL '7 days'
GROUP BY DATE_TRUNC('day', occurred_at)
ORDER BY DATE_TRUNC('day', occurred_at) ASC;
-- name: ListIncidentStatusCodes :many
SELECT COALESCE(http_status, 0) AS status_code, COALESCE(SUM(count), 0) AS total
FROM incident_events
WHERE occurred_at >= NOW() - INTERVAL '7 days'
GROUP BY COALESCE(http_status, 0)
ORDER BY total DESC, status_code ASC;
-- name: ListClientEventPaths :many
SELECT COALESCE((labels_json::jsonb ->> 'path'), 'unknown') AS path, COUNT(*) AS total
FROM metrics_timeseries
WHERE metric = 'client_event' AND occurred_at >= NOW() - INTERVAL '7 days'
GROUP BY COALESCE((labels_json::jsonb ->> 'path'), 'unknown')
ORDER BY total DESC, path ASC
LIMIT 20;
-- name: GetAPServiceBySlug :one
SELECT id, upstream_url, route_prefix, upstream_auth_header,
upstream_auth_value, enabled, rpm_limit, monthly_quota,
request_timeout_ms
FROM api_services WHERE slug = $1;
-- name: ListAPKeysByPrefix :many
SELECT id, key_hash, enabled, rpm_limit, monthly_quota, allowed_service_ids
FROM api_keys WHERE key_prefix = $1;
-- name: SumUsageByKey :one
SELECT COALESCE(SUM(request_count), 0)::bigint FROM usage_counters
WHERE api_key_id = $1 AND period_month = $2;
-- name: SumUsageByService :one
SELECT COALESCE(SUM(request_count), 0)::bigint FROM usage_counters
WHERE service_id = $1 AND period_month = $2;
-- name: UpsertUsageCounter :exec
INSERT INTO usage_counters (api_key_id, service_id, period_month, request_count, updated_at)
VALUES ($1, $2, $3, 1, NOW())
ON CONFLICT (api_key_id, service_id, period_month)
DO UPDATE SET request_count = usage_counters.request_count + 1, updated_at = NOW();
-- name: TouchAPKeyLastUsed :exec
UPDATE api_keys SET last_used_at = NOW() WHERE id = $1;
-- name: InsertMetricPoint :exec
INSERT INTO metrics_timeseries (metric, value, labels_json, occurred_at)
VALUES ('gateway_request', 1, $1, NOW());
-28
View File
@@ -1,28 +0,0 @@
-- name: InsertAuditLog :exec
INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, severity, category, label, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
-- name: InsertAuditLogWithRequest :exec
INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, ip_address, user_agent, severity, category, label, created_at)
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::inet, $8, $9, $10, $11, $12);
-- name: PruneAuditLogs :execrows
DELETE FROM audit_logs WHERE created_at < NOW() - ($1::int * INTERVAL '1 day');
-- name: ListAuditBackfillBatch :many
SELECT id::text, resource, action FROM audit_logs
WHERE label = '' AND id > $1::uuid ORDER BY id LIMIT 500;
-- name: SetAuditLogMetadata :exec
UPDATE audit_logs SET severity = $1, category = $2, label = $3 WHERE id = $4::uuid;
-- name: ListResourceAuditLogs :many
SELECT id, COALESCE(user_id::text, '')::text AS user_id, resource,
COALESCE(resource_id::text, '')::text AS resource_id, action,
COALESCE(details::text, '{}')::text AS details,
COALESCE(ip_address::text, '')::text AS ip_address,
COALESCE(user_agent, '') AS user_agent, created_at
FROM audit_logs
WHERE user_id::text = $1 AND resource = $2 AND resource_id::text = $3
ORDER BY created_at DESC
LIMIT 100;
-57
View File
@@ -1,57 +0,0 @@
-- name: GetUserByEmailForAuth :one
SELECT id, email, password_hash, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE email = $1;
-- name: CountUsers :one
SELECT COUNT(*) FROM users;
-- name: CountUsersByEmail :one
SELECT COUNT(*) FROM users WHERE email = $1;
-- name: UpsertLocalUser :one
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
VALUES ($1, $2, $3, NULLIF($4::varchar, ''), $5)
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name,
avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = NOW()
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at;
-- name: InsertUserAdmin :one
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, $4)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at;
-- name: InsertUser :one
INSERT INTO users (email, password_hash, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at;
-- name: GetUserByID :one
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE id = $1;
-- name: UpdateUserProfile :exec
UPDATE users
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
WHERE id = $3;
-- name: UpsertAdminUser :one
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, true)
ON CONFLICT (email) DO UPDATE
SET is_admin = true, updated_at = NOW()
RETURNING id;
-- name: ListAdminUsers :many
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin,
COALESCE(to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"'), '')::text AS created_at
FROM users
ORDER BY created_at ASC;
-- name: SetUserAdmin :execrows
UPDATE users SET is_admin = $1, updated_at = NOW() WHERE id = $2;
-- name: GetAdminUserInfo :one
SELECT email, COALESCE(name, '') AS name, is_admin
FROM users WHERE id = $1;
-33
View File
@@ -1,33 +0,0 @@
-- name: UpsertBuild :exec
INSERT INTO builds
(id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $14)
ON CONFLICT (id) DO UPDATE SET
project_id = EXCLUDED.project_id,
service_id = EXCLUDED.service_id,
status = EXCLUDED.status,
progress = EXCLUDED.progress,
started_at = EXCLUDED.started_at,
completed_at = EXCLUDED.completed_at,
image_name = EXCLUDED.image_name,
image_tag = EXCLUDED.image_tag,
size = EXCLUDED.size,
error = EXCLUDED.error,
log = EXCLUDED.log,
metadata = EXCLUDED.metadata,
updated_at = EXCLUDED.updated_at;
-- name: GetBuild :one
SELECT id, project_id, service_id, status, progress, started_at, completed_at, image_name, image_tag, size, error, log, metadata
FROM builds
WHERE id = $1;
-- name: CancelBuild :execrows
UPDATE builds
SET status = 'cancelled',
progress = 100,
completed_at = $1,
log = COALESCE(log, '') || $2,
updated_at = $1
WHERE id = $3
AND status NOT IN ('success', 'failed', 'cancelled');
-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;
-137
View File
@@ -1,137 +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);
-- name: GetGitRepoCloneByFullName :one
SELECT clone_url, provider_id FROM git_repositories
WHERE full_name = $1 AND user_id = $2 LIMIT 1;
-- name: GetGitRepoProviderByCloneURL :one
SELECT provider_id FROM git_repositories
WHERE clone_url = $1 AND user_id = $2 LIMIT 1;
-29
View File
@@ -1,29 +0,0 @@
-- name: CountProjects :one
SELECT COUNT(*) FROM projects;
-- name: GetRegistryAuth :one
SELECT username, password FROM registries
WHERE owner_id::text = $1 AND host = $2;
-- name: UpdateServiceRuntimeMeta :exec
UPDATE services SET port = $1, healthcheck_path = $2 WHERE id = $3;
-- name: UpdateServiceDeployMeta :exec
UPDATE services SET port = $1, healthcheck_path = $2,
volumes = COALESCE($3::jsonb, '[]'::jsonb),
git_repo = NULLIF($4::text, ''), git_branch = NULLIF($5::text, ''), builder = $6
WHERE id = $7;
-- name: InsertEnvVar :exec
INSERT INTO environment_variables (id, service_id, key, value, is_secret, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7);
-- name: DeleteProjectVariables :exec
DELETE FROM project_variables WHERE project_id = $1;
-- name: InsertProjectVariable :exec
INSERT INTO project_variables (id, project_id, key, value, is_secret, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7);
-- name: GetProjectVariableValue :one
SELECT value FROM project_variables WHERE project_id = $1 AND key = $2;
-43
View File
@@ -1,43 +0,0 @@
-- name: ListActiveOperationDeployments :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, d.error, 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 d.status IN ('queued','pending','building','deploying','rolling_back')
AND (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 ASC;
-- name: ListRecentFailedDeployments :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, d.error, 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 d.status IN ('failed','cancelled','rolled_back')
AND d.updated_at > NOW() - INTERVAL '24 hours'
AND (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.updated_at DESC
LIMIT 20;
-- name: ListRecentCronRuns :many
SELECT e.id, j.name AS job_name, j.schedule, e.status, e.started_at, e.finished_at, e.error
FROM cron_executions e
JOIN cron_jobs j ON j.id = e.cron_job_id
JOIN projects p ON p.id = j.project_id
WHERE e.started_at > NOW() - INTERVAL '24 hours'
AND (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 e.started_at DESC
LIMIT 30;
-- name: ListRecentOperationBackups :many
SELECT b.id, b.database_id, ds.name AS db_name, b.status, b.size, b.created_at, b.completed_at
FROM database_backups b
JOIN database_services ds ON ds.id = b.database_id
WHERE b.created_at > NOW() - INTERVAL '24 hours'
AND (ds.user_id = $1 OR $2::bool)
ORDER BY b.created_at DESC
LIMIT 20;
-59
View File
@@ -1,59 +0,0 @@
-- name: ListNodeAgentsForService :many
SELECT DISTINCT node_agent_id FROM container_instances WHERE service_id = $1;
-- name: ListServiceVariableValues :many
SELECT key, value FROM environment_variables WHERE service_id = $1;
-- name: ListServiceVariablesWithSecret :many
SELECT key, value, COALESCE(is_secret, false) AS is_secret
FROM environment_variables WHERE service_id = $1;
-- name: ListProjectServiceRefs :many
SELECT id, name FROM services WHERE project_id = $1;
-- name: GetServicePort :one
SELECT COALESCE(port, 0) AS port FROM services WHERE id = $1;
-- name: GetServiceVariableValue :one
SELECT value FROM environment_variables WHERE service_id = $1 AND key = $2;
-- name: GetServiceRuntimeWithOwner :one
SELECT s.id, s.project_id, s.name,
COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status,
COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command,
COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo,
COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path,
COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS memory,
COALESCE(s.replicas, 1) AS replicas, COALESCE(s.port, 0) AS port,
COALESCE(s.domain, '') AS domain, COALESCE(s.healthcheck_path, '') AS healthcheck_path,
COALESCE(s.restart_policy, 'unless-stopped') AS restart_policy,
COALESCE(s.builder, 'auto') AS builder, COALESCE(s.cpu_reserve, '') AS cpu_reserve,
COALESCE(s.memory_reserve, '') AS memory_reserve, COALESCE(s.static_build_cmd, '') AS static_build_cmd,
COALESCE(s.static_dir, '') AS static_dir,
s.created_at, s.updated_at, p.owner_id
FROM services s
JOIN projects p ON s.project_id = p.id
WHERE s.id = $1;
-- name: GetLastDeployedImage :one
SELECT (image_name || ':' || image_tag)::text AS image FROM deployments
WHERE service_id = $1 AND status = 'deployed' AND image_name <> ''
ORDER BY created_at DESC LIMIT 1;
-- name: SetServicePublishedPort :exec
UPDATE services SET published_port = $1 WHERE id = $2 AND published_port <> $1;
-- name: SetServiceReplicas :exec
UPDATE services SET replicas = $1, updated_at = $2 WHERE id = $3;
-- name: ListSleepCandidates :many
SELECT id, project_id, name, COALESCE(status,'') AS status,
COALESCE(sleep_idle_minutes, 15) AS sleep_idle_minutes
FROM services
WHERE sleep_enabled AND COALESCE(status,'') IN ('running','deployed','degraded','starting');
-- name: GetServiceDomainText :one
SELECT COALESCE(domain,'') AS domain FROM services WHERE id = $1;
-- name: GetServiceStatusText :one
SELECT COALESCE(status,'') AS status FROM services WHERE id = $1;
-78
View File
@@ -1,78 +0,0 @@
-- name: GetProjectOwnerText :one
SELECT owner_id::text FROM projects WHERE id = $1;
-- name: ServiceExistsInProject :one
SELECT EXISTS(SELECT 1 FROM services WHERE id = $1 AND project_id = $2);
-- name: ListVulnerabilities :many
SELECT id, type, severity, title, description, service_id, status, found_at, resolved_at
FROM vulnerabilities
WHERE project_id = $1
ORDER BY
CASE severity
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
found_at DESC;
-- name: UpdateVulnerabilityStatus :exec
UPDATE vulnerabilities SET status = $1, resolved_at = $2 WHERE id = $3;
-- name: ComplianceFrameworkExists :one
SELECT EXISTS(SELECT 1 FROM compliance_frameworks WHERE id = $1);
-- name: ListComplianceFrameworks :many
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name;
-- name: GetVulnerabilityMetrics :one
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE severity = 'critical') AS critical,
COUNT(*) FILTER (WHERE severity = 'high') AS high,
COUNT(*) FILTER (WHERE severity = 'medium') AS medium,
COUNT(*) FILTER (WHERE severity = 'low') AS low,
COUNT(*) FILTER (WHERE status = 'open') AS open,
COUNT(*) FILTER (WHERE status = 'resolved') AS resolved
FROM vulnerabilities
WHERE project_id = $1;
-- name: GetLatestSecurityScan :one
SELECT id, COALESCE((summary->>'score')::int, 0)::int AS score, started_at AS scanned_at, status
FROM security_scans
WHERE project_id = $1
ORDER BY started_at DESC
LIMIT 1;
-- name: GetLatestComplianceReport :one
SELECT overall_status, score, assessment_date
FROM compliance_reports
WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1;
-- name: CheckSecurityProjectAccess :one
SELECT EXISTS (
SELECT 1
FROM projects p
WHERE p.id = $1
AND (p.is_approved
OR $3::bool
OR p.owner_id = $2 OR EXISTS (
SELECT 1 FROM project_members pm
WHERE pm.project_id = p.id AND pm.user_id = $2
))
);
-- name: GetSecurityScanProjectID :one
SELECT project_id FROM security_scans WHERE id = $1;
-- name: GetComplianceReportProjectID :one
SELECT project_id FROM compliance_reports WHERE id = $1;
-- name: GetVulnerabilityProjectID :one
SELECT project_id FROM vulnerabilities WHERE id = $1;
@@ -1,62 +0,0 @@
-- name: GetServiceSleep :one
SELECT COALESCE(sleep_enabled, false) AS sleep_enabled, COALESCE(sleep_idle_minutes, 15) AS sleep_idle_minutes
FROM services WHERE id = $1;
-- name: GetServiceNodeID :one
SELECT node_id FROM services WHERE id = $1;
-- name: GetServiceSpread :one
SELECT COALESCE(spread, false) FROM services WHERE id = $1;
-- name: GetServicePlacementTags :one
SELECT placement_tags FROM services WHERE id = $1;
-- name: GetServiceTraefikLabels :one
SELECT traefik_labels FROM services WHERE id = $1;
-- name: GetServiceVolumes :one
SELECT volumes FROM services WHERE id = $1;
-- name: GetNodeAgentName :one
SELECT name FROM node_agents WHERE id = $1;
-- name: GetServiceProjectID :one
SELECT project_id FROM services WHERE id = $1;
-- name: GetServiceProjectAndStatus :one
SELECT s.project_id, COALESCE(s.status, '') AS status
FROM services s WHERE s.id = $1;
-- name: GetEnvironmentProjectID :one
SELECT project_id FROM environments WHERE id = $1;
-- name: GetProjectReadAccess :one
SELECT is_approved, owner_id::text FROM projects WHERE id = $1;
-- name: ProjectMemberExists :one
SELECT EXISTS(SELECT 1 FROM project_members WHERE project_id = $1 AND user_id = $2);
-- name: DeleteEnvVarsForService :exec
DELETE FROM environment_variables WHERE service_id = $1;
-- name: FailInterruptedDeployments :exec
UPDATE deployments SET status = 'failed', error = 'Interrupted by server restart', completed_at = NOW(), updated_at = NOW()
WHERE status IN ('queued', 'pending', 'building', 'deploying', 'rolling_back');
-- name: ListSecretEnvVars :many
SELECT id, value FROM environment_variables WHERE is_secret = TRUE;
-- name: UpdateEnvVarValue :exec
UPDATE environment_variables SET value = $1, updated_at = NOW() WHERE id = $2;
-- name: ListAgentHealthRows :many
SELECT id, COALESCE(name, hostname, id::text) AS name, COALESCE(status, '') AS status,
COALESCE(last_heartbeat < $1, TRUE)::bool AS stale, auto_prune
FROM node_agents;
-- name: SetAgentStatus :exec
UPDATE node_agents SET status = $1, updated_at = NOW() WHERE id = $2;
-- name: GetServiceNameProjectPort :one
SELECT name, project_id::text, COALESCE(port, 0)::int AS port
FROM services WHERE id::text = $1;
-21
View File
@@ -1,21 +0,0 @@
-- name: GetAppSetting :one
SELECT value FROM app_settings WHERE key = $1;
-- name: UpsertAppSetting :exec
INSERT INTO app_settings (key, value, is_secret, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value, is_secret = EXCLUDED.is_secret, updated_at = NOW();
-- name: UpsertAppSettingValue :exec
INSERT INTO app_settings (key, value, updated_at) VALUES ($1, $2, NOW())
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW();
-- name: DeleteAppSetting :exec
DELETE FROM app_settings WHERE key = $1;
-- name: GetUserIsAdmin :one
SELECT is_admin FROM users WHERE id = $1;
-- name: ListAdminUserIDs :many
SELECT id FROM users WHERE is_admin;
-251
View File
@@ -69,8 +69,6 @@ CREATE TABLE services (
static_dir VARCHAR(255) NOT NULL DEFAULT '', static_dir VARCHAR(255) NOT NULL DEFAULT '',
node_id VARCHAR(255), node_id VARCHAR(255),
spread BOOLEAN NOT NULL DEFAULT false, spread BOOLEAN NOT NULL DEFAULT false,
sleep_enabled BOOLEAN NOT NULL DEFAULT false,
sleep_idle_minutes INTEGER NOT NULL DEFAULT 15,
placement_tags JSONB NOT NULL DEFAULT '[]', placement_tags JSONB NOT NULL DEFAULT '[]',
traefik_labels JSONB NOT NULL DEFAULT '{}' traefik_labels JSONB NOT NULL DEFAULT '{}'
); );
@@ -144,17 +142,6 @@ CREATE TABLE environment_variables (
UNIQUE(service_id, key) UNIQUE(service_id, key)
); );
CREATE TABLE project_variables (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
key VARCHAR(255) NOT NULL,
value TEXT NOT NULL DEFAULT '',
is_secret BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
UNIQUE(project_id, key)
);
CREATE TABLE service_templates ( CREATE TABLE service_templates (
id VARCHAR(50) PRIMARY KEY, id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
@@ -472,241 +459,3 @@ CREATE TABLE instance_metrics (
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (service_id, timestamp, instance_id) PRIMARY KEY (service_id, timestamp, instance_id)
); );
CREATE TABLE cron_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
service_id UUID NOT NULL REFERENCES services(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
schedule VARCHAR(100) NOT NULL,
command TEXT NOT NULL,
timezone VARCHAR(50) DEFAULT 'UTC',
enabled BOOLEAN DEFAULT true,
last_run_at TIMESTAMP WITH TIME ZONE,
next_run_at TIMESTAMP WITH TIME ZONE,
last_status VARCHAR(50),
last_output TEXT,
retention INTEGER DEFAULT 30,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE cron_executions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cron_job_id UUID NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
started_at TIMESTAMP WITH TIME ZONE NOT NULL,
finished_at TIMESTAMP WITH TIME ZONE,
status VARCHAR(50) DEFAULT 'pending',
output TEXT,
error TEXT
);
CREATE TABLE git_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
display_name VARCHAR(255) NOT NULL,
api_url VARCHAR(255) NOT NULL,
webhook_url VARCHAR(255) NOT NULL,
access_token TEXT NOT NULL,
user_id UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE git_repositories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
full_name VARCHAR(255) NOT NULL,
description TEXT,
clone_url VARCHAR(255) NOT NULL,
webhook_url VARCHAR(255),
default_branch VARCHAR(255) DEFAULT 'main',
is_private BOOLEAN DEFAULT false,
user_id UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE git_webhooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
repo_id UUID NOT NULL,
provider_id UUID NOT NULL,
events TEXT NOT NULL,
webhook_secret TEXT NOT NULL,
remote_webhook_id VARCHAR(255),
active BOOLEAN DEFAULT true,
branch_filter VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
avatar_url VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_admin BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE compliance_frameworks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
version VARCHAR(20) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE security_scans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL,
service_id UUID,
scan_type VARCHAR(50) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'running',
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
summary JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE compliance_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL,
framework_id UUID NOT NULL,
assessment_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
assessor VARCHAR(255),
overall_status VARCHAR(50) NOT NULL,
score INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE vulnerabilities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(50) NOT NULL,
severity VARCHAR(20) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
service_id UUID,
project_id UUID NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'open',
found_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE app_settings (
key VARCHAR(255) PRIMARY KEY,
value TEXT NOT NULL DEFAULT '',
is_secret BOOLEAN NOT NULL DEFAULT false,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
user_id UUID,
action VARCHAR(100) NOT NULL,
resource VARCHAR(100) NOT NULL,
resource_id UUID,
details JSONB DEFAULT '{}',
ip_address INET,
user_agent TEXT,
success BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
severity TEXT NOT NULL DEFAULT 'info',
category TEXT NOT NULL DEFAULT '',
label TEXT NOT NULL DEFAULT ''
);
CREATE TABLE builds (
id VARCHAR(255) PRIMARY KEY,
project_id VARCHAR(255),
service_id VARCHAR(255),
status VARCHAR(50) NOT NULL DEFAULT 'pending',
progress INTEGER NOT NULL DEFAULT 0,
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
image_name VARCHAR(500) NOT NULL DEFAULT '',
image_tag VARCHAR(200) NOT NULL DEFAULT '',
size BIGINT NOT NULL DEFAULT 0,
error TEXT,
log TEXT NOT NULL DEFAULT '',
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- APwhy API gateway (api_services/api_keys + usage/metrics/incident planes)
CREATE TABLE api_services (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
upstream_url TEXT NOT NULL,
route_prefix TEXT NOT NULL UNIQUE,
health_path TEXT NOT NULL DEFAULT '/health',
upstream_auth_header TEXT,
upstream_auth_value TEXT,
internal_token TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
rpm_limit INTEGER,
monthly_quota INTEGER,
request_timeout_ms INTEGER DEFAULT 8000,
last_validation_at TIMESTAMP WITHOUT TIME ZONE,
last_validation_status TEXT,
last_validation_message TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
);
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL,
plan TEXT NOT NULL DEFAULT 'free',
allowed_service_ids TEXT NOT NULL DEFAULT '[]',
enabled INTEGER NOT NULL DEFAULT 1,
rpm_limit INTEGER DEFAULT 60,
monthly_quota INTEGER DEFAULT 1000,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
last_used_at TIMESTAMP WITHOUT TIME ZONE
);
CREATE TABLE usage_counters (
id INTEGER PRIMARY KEY,
api_key_id UUID NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
service_id UUID NOT NULL REFERENCES api_services(id) ON DELETE CASCADE,
period_month TEXT NOT NULL,
request_count INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
UNIQUE (api_key_id, service_id, period_month)
);
CREATE TABLE metrics_timeseries (
id INTEGER PRIMARY KEY,
metric TEXT NOT NULL,
value REAL NOT NULL,
labels_json TEXT,
occurred_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
);
CREATE TABLE incident_events (
id INTEGER PRIMARY KEY,
service_id UUID REFERENCES api_services(id) ON DELETE SET NULL,
api_key_id UUID,
code TEXT NOT NULL,
message TEXT NOT NULL,
severity TEXT NOT NULL DEFAULT 'medium',
http_status INTEGER,
count INTEGER NOT NULL DEFAULT 1,
occurred_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
);
@@ -25,7 +25,7 @@ const SEVERITY_STYLE: Record<string, { icon: typeof Info; cls: string; dot: stri
}; };
const SCHEMA: FilterConfig<ActivityEntry>[] = [ const SCHEMA: FilterConfig<ActivityEntry>[] = [
{ key: 'q', label: 'events', type: 'search', searchFields: ['label', 'action', 'user_email'] }, { key: 'q', label: 'Search', type: 'search', searchFields: ['label', 'action', 'user_email'] },
{ {
key: 'severity', label: 'Severity', type: 'select', key: 'severity', label: 'Severity', type: 'select',
options: [ options: [
@@ -67,7 +67,7 @@ export function ActivityPage() {
</div> </div>
</div> </div>
<div className="px-4 py-6 sm:px-8 space-y-4"> <div className="px-8 py-6 space-y-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex-1"> <div className="flex-1">
<FilterBar {...filter} /> <FilterBar {...filter} />
@@ -85,7 +85,7 @@ export function AuditLogsPage() {
</div> </div>
</div> </div>
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<section className="panel p-6"> <section className="panel p-6">
<FilterBar {...filter} /> <FilterBar {...filter} />
@@ -140,7 +140,7 @@ export function AuditLogsPage() {
<td className="px-3 py-2.5 mono text-xs text-[var(--text-tertiary)] max-w-[140px] truncate" title={log.resourceId}> <td className="px-3 py-2.5 mono text-xs text-[var(--text-tertiary)] max-w-[140px] truncate" title={log.resourceId}>
{log.resourceId || '—'} {log.resourceId || '—'}
</td> </td>
<td className="px-3 py-2.5 mono text-xs text-[var(--text-tertiary)]">{log.ipAddress?.replace(/\/\d+$/, '') || '—'}</td> <td className="px-3 py-2.5 mono text-xs text-[var(--text-tertiary)]">{log.ipAddress || '—'}</td>
<td className="px-3 py-2.5 mono text-xs text-[var(--text-tertiary)] max-w-[220px] truncate" title={log.details}> <td className="px-3 py-2.5 mono text-xs text-[var(--text-tertiary)] max-w-[220px] truncate" title={log.details}>
{log.details && log.details !== '{}' ? log.details : '—'} {log.details && log.details !== '{}' ? log.details : '—'}
</td> </td>
@@ -192,7 +192,7 @@ export function BuildsPage() {
)} )}
{/* Stats Overview */} {/* Stats Overview */}
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="panel p-4"> <div className="panel p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
@@ -263,7 +263,7 @@ export function BuildsPage() {
</div> </div>
{/* Build Table */} {/* Build Table */}
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<div className="panel overflow-hidden"> <div className="panel overflow-hidden">
{!isDemoMode && buildsQuery.isLoading ? ( {!isDemoMode && buildsQuery.isLoading ? (
<div className="p-12 text-center"> <div className="p-12 text-center">
@@ -15,7 +15,6 @@ function renderMarkdown(markdown: string): string {
function excerpt(markdown: string, length = 140): string { function excerpt(markdown: string, length = 140): string {
const text = markdown const text = markdown
.replace(/^#[^\n]*\n/, ' ')
.replace(/```[\s\S]*?```/g, ' ') .replace(/```[\s\S]*?```/g, ' ')
.replace(/[#>*`\-[\]()]/g, ' ') .replace(/[#>*`\-[\]()]/g, ' ')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
@@ -120,7 +119,7 @@ export function DocsBrowser() {
<div className="min-w-0 flex-1 overflow-y-auto max-md:hidden"> <div className="min-w-0 flex-1 overflow-y-auto max-md:hidden">
{selected ? ( {selected ? (
<article className="docs-article px-4 py-6 sm:px-8"> <article className="docs-article px-8 py-6">
<p className="mb-1 text-[11px] font-medium uppercase tracking-wider text-[var(--text-tertiary)]"> <p className="mb-1 text-[11px] font-medium uppercase tracking-wider text-[var(--text-tertiary)]">
{selected.section} / {selected.path} {selected.section} / {selected.path}
</p> </p>
@@ -330,7 +330,7 @@ export function NodeDetailPage() {
</div> </div>
</div> </div>
<div className="px-4 py-6 sm:px-8 space-y-6"> <div className="px-8 py-6 space-y-6">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Stat label="cpu" value={latest ? `${latest.cpu.usage_percent.toFixed(0)}%` : '—'} sub={latest ? `${latest.cpu.cores} cores` : ''} /> <Stat label="cpu" value={latest ? `${latest.cpu.usage_percent.toFixed(0)}%` : '—'} sub={latest ? `${latest.cpu.cores} cores` : ''} />
<Stat label="memory" value={latest ? `${latest.memory.usage_percent.toFixed(0)}%` : '—'} sub={latest ? `${formatBytes(latest.memory.available)} free of ${formatBytes(latest.memory.limit)}` : ''} /> <Stat label="memory" value={latest ? `${latest.memory.usage_percent.toFixed(0)}%` : '—'} sub={latest ? `${formatBytes(latest.memory.available)} free of ${formatBytes(latest.memory.limit)}` : ''} />
@@ -88,7 +88,7 @@ export function OperationsPage() {
<Loader2 size={20} className="animate-spin text-[var(--text-muted)]" /> <Loader2 size={20} className="animate-spin text-[var(--text-muted)]" />
</div> </div>
) : ( ) : (
<div className="w-full px-4 py-6 sm:px-8 space-y-6"> <div className="w-full px-8 py-6 space-y-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="panel-soft p-4"> <div className="panel-soft p-4">
<p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">In flight</p> <p className="text-xs uppercase tracking-wider text-[var(--text-muted)]">In flight</p>
@@ -211,9 +211,9 @@ export function ProjectsPage() {
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
{/* Page head */} {/* Page head */}
<div className="mb-6 flex flex-wrap items-center justify-between gap-x-5 gap-y-3"> <div className="mb-6 flex items-center justify-between gap-5">
<div> <div>
<h1 className="v-title">Projects<span className="v-cursor">_</span></h1> <h1 className="v-title">Projects<span className="v-cursor">_</span></h1>
<p className="v-mono mt-1.5 text-[11px] text-[var(--text-tertiary)]"> <p className="v-mono mt-1.5 text-[11px] text-[var(--text-tertiary)]">
+32 -36
View File
@@ -436,7 +436,7 @@ export function UsagePage() {
title="Usage" title="Usage"
description="Platform usage metrics and operational summaries" description="Platform usage metrics and operational summaries"
/> />
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">
<div className={`w-2 h-2 rounded-full ${liveStatus === 'live' ? 'bg-[var(--success)] animate-pulse' : 'bg-[var(--text-muted)]'}`} /> <div className={`w-2 h-2 rounded-full ${liveStatus === 'live' ? 'bg-[var(--success)] animate-pulse' : 'bg-[var(--text-muted)]'}`} />
<span className={`text-sm ${liveStatus === 'live' ? 'text-[var(--success)]' : 'text-[var(--text-muted)]'}`}> <span className={`text-sm ${liveStatus === 'live' ? 'text-[var(--success)]' : 'text-[var(--text-muted)]'}`}>
@@ -844,7 +844,7 @@ export function PeoplePage() {
title="People" title="People"
description="Team management and access control" description="Team management and access control"
/> />
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
{profileQuery.isLoading ? ( {profileQuery.isLoading ? (
<div className="py-16 text-center"> <div className="py-16 text-center">
<Loader2 size={24} className="animate-spin mx-auto text-[var(--text-tertiary)]" /> <Loader2 size={24} className="animate-spin mx-auto text-[var(--text-tertiary)]" />
@@ -888,7 +888,7 @@ export function PeoplePage() {
</div> </div>
<form <form
className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_auto]" className="grid gap-3 lg:grid-cols-[1fr_1fr_1fr_auto]"
onSubmit={(event) => { onSubmit={(event) => {
event.preventDefault(); event.preventDefault();
createUserMutation.mutate(); createUserMutation.mutate();
@@ -2145,7 +2145,7 @@ export function SettingsPage() {
title="Settings" title="Settings"
description="Manage account profile and local configuration" description="Manage account profile and local configuration"
/> />
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6"> <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
{/* Profile Section */} {/* Profile Section */}
<section className="panel p-6"> <section className="panel p-6">
@@ -2376,7 +2376,7 @@ export function DocsPage() {
title="Docs" title="Docs"
description="Guides and references — synced from GitHub, cached offline" description="Guides and references — synced from GitHub, cached offline"
/> />
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<DocsBrowser /> <DocsBrowser />
</div> </div>
</div> </div>
@@ -2513,7 +2513,7 @@ export function DatabasesPage() {
title="Databases" title="Databases"
description="Managed database services — connection info, runtime actions and backups" description="Managed database services — connection info, runtime actions and backups"
/> />
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<div className="mb-5 flex items-center justify-between"> <div className="mb-5 flex items-center justify-between">
<p className="text-xs text-[var(--text-tertiary)]"> <p className="text-xs text-[var(--text-tertiary)]">
{databases.length} {databases.length === 1 ? 'database' : 'databases'} {databases.length} {databases.length === 1 ? 'database' : 'databases'}
@@ -3251,7 +3251,7 @@ export function HighAvailabilityPage() {
title="High availability" title="High availability"
description="Failover manager, policies, and active alerts" description="Failover manager, policies, and active alerts"
/> />
<div className="p-4 space-y-8 sm:p-8"> <div className="p-8 space-y-8">
<div className="flex items-center gap-2 text-xs text-[var(--text-tertiary)]"> <div className="flex items-center gap-2 text-xs text-[var(--text-tertiary)]">
<ShieldCheck size={13} className="text-[var(--accent-primary)]" /> <ShieldCheck size={13} className="text-[var(--accent-primary)]" />
<span>HA state feeds the platform security posture.</span> <span>HA state feeds the platform security posture.</span>
@@ -3615,43 +3615,39 @@ export function SecurityPage() {
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects(), enabled: !isDemoMode }); const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects(), enabled: !isDemoMode });
const projects = isDemoMode ? demoProjects : projectsQuery.data ?? []; const projects = isDemoMode ? demoProjects : projectsQuery.data ?? [];
// Auto-select the first project so the page never renders empty — derived, not
// stored, so clearing the select still shows the empty state explicitly.
const effectiveProjectId = projectId || projects[0]?.id || '';
const servicesQuery = useQuery({ const servicesQuery = useQuery({
queryKey: ['services', effectiveProjectId], queryKey: ['services', projectId],
queryFn: () => listServicesByProject(effectiveProjectId), queryFn: () => listServicesByProject(projectId),
enabled: !isDemoMode && Boolean(effectiveProjectId), enabled: !isDemoMode && Boolean(projectId),
}); });
const services = isDemoMode ? getDemoServicesByProject(effectiveProjectId) : servicesQuery.data ?? []; const services = isDemoMode ? getDemoServicesByProject(projectId) : servicesQuery.data ?? [];
const metricsQuery = useQuery({ const metricsQuery = useQuery({
queryKey: ['security-metrics', effectiveProjectId], queryKey: ['security-metrics', projectId],
queryFn: () => getSecurityMetrics(effectiveProjectId), queryFn: () => getSecurityMetrics(projectId),
enabled: !isDemoMode && Boolean(effectiveProjectId), enabled: !isDemoMode && Boolean(projectId),
refetchInterval: 15_000, refetchInterval: 15_000,
}); });
const historyQuery = useQuery({ const historyQuery = useQuery({
queryKey: ['security-history', effectiveProjectId], queryKey: ['security-history', projectId],
queryFn: () => getSecurityHistory(effectiveProjectId), queryFn: () => getSecurityHistory(projectId),
enabled: !isDemoMode && Boolean(effectiveProjectId), enabled: !isDemoMode && Boolean(projectId),
refetchInterval: 15_000, refetchInterval: 15_000,
}); });
const vulnsQuery = useQuery({ const vulnsQuery = useQuery({
queryKey: ['security-vulns', effectiveProjectId], queryKey: ['security-vulns', projectId],
queryFn: () => listVulnerabilities(effectiveProjectId), queryFn: () => listVulnerabilities(projectId),
enabled: !isDemoMode && Boolean(effectiveProjectId), enabled: !isDemoMode && Boolean(projectId),
}); });
const invalidate = () => { const invalidate = () => {
queryClient.invalidateQueries({ queryKey: ['security-metrics', effectiveProjectId] }); queryClient.invalidateQueries({ queryKey: ['security-metrics', projectId] });
queryClient.invalidateQueries({ queryKey: ['security-history', effectiveProjectId] }); queryClient.invalidateQueries({ queryKey: ['security-history', projectId] });
queryClient.invalidateQueries({ queryKey: ['security-vulns', effectiveProjectId] }); queryClient.invalidateQueries({ queryKey: ['security-vulns', projectId] });
}; };
const scanMutation = useMutation({ const scanMutation = useMutation({
mutationFn: () => startSecurityScan({ mutationFn: () => startSecurityScan({
project_id: effectiveProjectId, project_id: projectId,
service_id: scanServiceId || undefined, service_id: scanServiceId || undefined,
scan_type: scanType as 'dependency' | 'configuration' | 'comprehensive', scan_type: scanType as 'dependency' | 'configuration' | 'comprehensive',
}), }),
@@ -3665,10 +3661,10 @@ export function SecurityPage() {
const metrics = isDemoMode ? demoSecurityMetrics : metricsQuery.data; const metrics = isDemoMode ? demoSecurityMetrics : metricsQuery.data;
const scanHistory = isDemoMode const scanHistory = isDemoMode
? demoSecurityScans.filter((s) => s.project_id === effectiveProjectId) ? demoSecurityScans.filter((s) => s.project_id === projectId)
: historyQuery.data ?? []; : historyQuery.data ?? [];
const vulns = (isDemoMode const vulns = (isDemoMode
? demoVulnerabilities.filter((v) => v.project_id === effectiveProjectId) ? demoVulnerabilities.filter((v) => v.project_id === projectId)
: vulnsQuery.data ?? [] : vulnsQuery.data ?? []
).filter((v) => v.status !== 'resolved'); ).filter((v) => v.status !== 'resolved');
const pageError = const pageError =
@@ -3681,7 +3677,7 @@ export function SecurityPage() {
title="Security" title="Security"
description="Scan findings, security posture, and compliance status" description="Scan findings, security posture, and compliance status"
/> />
<div className="p-4 space-y-8 sm:p-8"> <div className="p-8 space-y-8">
<div className="flex items-center gap-2 text-xs text-[var(--text-tertiary)]"> <div className="flex items-center gap-2 text-xs text-[var(--text-tertiary)]">
<HeartPulse size={13} className="text-[var(--accent-primary)]" /> <HeartPulse size={13} className="text-[var(--accent-primary)]" />
<span>Platform availability and failover live under High availability.</span> <span>Platform availability and failover live under High availability.</span>
@@ -3699,7 +3695,7 @@ export function SecurityPage() {
<div> <div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Project</label> <label className="block text-xs text-[var(--text-tertiary)] mb-1">Project</label>
<select <select
value={effectiveProjectId} value={projectId}
onChange={(e) => { setProjectId(e.target.value); setScanServiceId(''); }} onChange={(e) => { setProjectId(e.target.value); setScanServiceId(''); }}
className="px-3 py-2 rounded-[var(--radius-md)] bg-[var(--surface-muted)] border border-[var(--border-subtle)] text-sm text-[var(--text-primary)] w-56" className="px-3 py-2 rounded-[var(--radius-md)] bg-[var(--surface-muted)] border border-[var(--border-subtle)] text-sm text-[var(--text-primary)] w-56"
> >
@@ -3709,7 +3705,7 @@ export function SecurityPage() {
))} ))}
</select> </select>
</div> </div>
{effectiveProjectId && ( {projectId && (
<> <>
<div> <div>
<label className="block text-xs text-[var(--text-tertiary)] mb-1">Scan type</label> <label className="block text-xs text-[var(--text-tertiary)] mb-1">Scan type</label>
@@ -3752,7 +3748,7 @@ export function SecurityPage() {
)} )}
</div> </div>
{effectiveProjectId && metrics && ( {projectId && metrics && (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard <StatCard
title="Security score" title="Security score"
@@ -3783,7 +3779,7 @@ export function SecurityPage() {
</div> </div>
)} )}
{effectiveProjectId && ( {projectId && (
<div className="panel p-6"> <div className="panel p-6">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<Shield size={18} className="text-[var(--accent-primary)]" /> <Shield size={18} className="text-[var(--accent-primary)]" />
@@ -3839,7 +3835,7 @@ export function SecurityPage() {
</div> </div>
)} )}
{effectiveProjectId && ( {projectId && (
<div className="panel p-6"> <div className="panel p-6">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<Clock size={18} className="text-[var(--accent-primary)]" /> <Clock size={18} className="text-[var(--accent-primary)]" />
@@ -143,7 +143,7 @@ function StatusBadge({ status }: { status: string }) {
export function ServiceDetailPage() { export function ServiceDetailPage() {
const { projectId = '', serviceId = '' } = useParams<{ projectId: string; serviceId: string }>(); const { projectId = '', serviceId = '' } = useParams<{ projectId: string; serviceId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isDemoMode = useDemoMode(); const isDemoMode = useDemoMode();
const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const sessionQuery = useAuthSession({ enabled: !isDemoMode });
@@ -791,7 +791,7 @@ export function ServiceDetailPage() {
)} )}
{/* Metrics Overview */} {/* Metrics Overview */}
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<EnhancedMetricCard <EnhancedMetricCard
title="CPU Usage" title="CPU Usage"
@@ -904,18 +904,7 @@ export function ServiceDetailPage() {
return ( return (
<button <button
key={item.key} key={item.key}
onClick={() => { onClick={() => setActiveSection(item.key)}
setActiveSection(item.key);
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
if (item.key === 'metrics') next.delete('section');
else next.set('section', item.key);
return next;
},
{ replace: true },
);
}}
className={`tab ${active ? 'active' : ''}`} className={`tab ${active ? 'active' : ''}`}
> >
<Icon size={14} /> <Icon size={14} />
@@ -1200,7 +1189,7 @@ export function ServiceDetailPage() {
{' '} {' '}
<span className="text-[var(--accent-primary)]">{entry.stream}</span> <span className="text-[var(--accent-primary)]">{entry.stream}</span>
{' '} {' '}
{entry.message.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z?\s+/, '')} {entry.message}
</p> </p>
))} ))}
</div> </div>
@@ -1373,7 +1362,7 @@ export function ServiceDetailPage() {
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_auto] gap-2 px-1"> <div className="grid grid-cols-[1fr_1fr_auto_auto] gap-2 px-1">
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Key</span> <span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Key</span>
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Value</span> <span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Value</span>
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Secret</span> <span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Secret</span>
@@ -1381,7 +1370,7 @@ export function ServiceDetailPage() {
</div> </div>
{varRows.map((row, i) => ( {varRows.map((row, i) => (
<div key={i}> <div key={i}>
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_auto] gap-2 items-center"> <div className="grid grid-cols-[1fr_1fr_auto_auto] gap-2 items-center">
<input <input
value={row.key} value={row.key}
onChange={(e) => updateVarRow(i, { key: e.target.value })} onChange={(e) => updateVarRow(i, { key: e.target.value })}
@@ -541,7 +541,7 @@ export function TemplatesPage() {
)} )}
{/* Main Content */} {/* Main Content */}
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<div className="grid grid-cols-1 xl:grid-cols-[380px_1fr] gap-6"> <div className="grid grid-cols-1 xl:grid-cols-[380px_1fr] gap-6">
{/* Template List */} {/* Template List */}
<section className="panel overflow-hidden"> <section className="panel overflow-hidden">
@@ -857,11 +857,7 @@ export function TemplatesPage() {
</div> </div>
</div> </div>
{sessionQuery.isPending && !isDemoMode ? ( {!signedIn ? (
<div className="rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-4 py-5 text-center">
<Loader2 size={16} className="mx-auto animate-spin text-[var(--text-muted)]" />
</div>
) : !signedIn ? (
<div className="rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-4 py-5 text-center"> <div className="rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-4 py-5 text-center">
<p className="text-sm text-[var(--text-secondary)]"> <p className="text-sm text-[var(--text-secondary)]">
Sign in to deploy this template into one of your projects. Sign in to deploy this template into one of your projects.
@@ -226,7 +226,7 @@ export function WebhooksPage() {
</div> </div>
</div> </div>
<div className="w-full px-4 py-6 sm:px-8 space-y-4 max-w-4xl"> <div className="w-full px-8 py-6 space-y-4 max-w-4xl">
{secret && ( {secret && (
<div className="panel-soft p-4 flex items-center gap-3"> <div className="panel-soft p-4 flex items-center gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -1,205 +1,306 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useState } from 'react';
import { useQueries } from '@tanstack/react-query'; import {
import { Cpu, HardDrive, ArrowDownUp, Boxes } from 'lucide-react'; LineMetricChart,
import { LineMetricChart, DonutChart } from '@/shared/components'; DualLineChart,
import { getServiceMetrics, type ServiceMetrics } from '@/lib/api-client'; DonutChart,
import { formatBytes } from '@/lib/time'; } from '@/shared/components';
interface MetricData {
cpu: number[];
ram: number;
ramUsed: string;
cache: number;
cacheBreakdown: { cache: number; nonCache: number; total: number };
users: number[];
performance: number[];
performanceAlt: number[];
upSpeed: number;
downSpeed: number;
}
interface MetricsDashboardProps { interface MetricsDashboardProps {
services: { id: string; name: string; status: string }[]; projectId?: string;
isDemoMode?: boolean; isRunning?: boolean;
onStop?: () => void;
onRestart?: () => void;
} }
const cardStyle: React.CSSProperties = { function getRandom(min: number, max: number) {
background: 'var(--surface-card, rgba(255,255,255,0.03))', return Math.floor(Math.random() * (max - min + 1)) + min;
border: '1px solid var(--border-subtle, rgba(255,255,255,0.09))',
borderRadius: '14px',
padding: '18px 20px',
};
function healthLabel(pct: number): { text: string; color: string } {
if (pct < 50) return { text: 'Good', color: 'var(--success, #5ee6a0)' };
if (pct < 80) return { text: 'Average', color: 'var(--warning, #f2c94c)' };
return { text: 'High', color: 'var(--error, #ff6b6b)' };
} }
export function MetricsDashboard({ services, isDemoMode = false }: MetricsDashboardProps) { function generateInitialData(): MetricData {
const liveServices = services.filter((s) => s.status === 'running'); return {
cpu: Array.from({ length: 24 }, () => getRandom(10, 45)),
ram: getRandom(50, 85),
ramUsed: '5.4 GB',
cache: 352,
cacheBreakdown: { cache: 212, nonCache: 85.5, total: 1750 },
users: Array.from({ length: 15 }, () => getRandom(40, 110)),
performance: Array.from({ length: 12 }, () => getRandom(70, 95)),
performanceAlt: Array.from({ length: 12 }, () => getRandom(60, 85)),
upSpeed: 10.4,
downSpeed: 5.2,
};
}
const metricsQueries = useQueries({ export function MetricsDashboard({
queries: liveServices.map((service) => ({ isRunning = true,
queryKey: ['service-metrics', service.id], }: MetricsDashboardProps) {
queryFn: () => getServiceMetrics(service.id), const [data, setData] = useState<MetricData>(generateInitialData);
enabled: !isDemoMode, const [timeRange, setTimeRange] = useState<'day' | 'month' | 'year'>('day');
refetchInterval: 5000,
retry: false,
})),
});
const samples = metricsQueries
.map((q) => q.data)
.filter((m): m is ServiceMetrics => Boolean(m) && m!.status === 'ok');
const totalCpu = samples.reduce((sum, m) => sum + m.cpu_percent, 0);
const avgCpu = samples.length > 0 ? totalCpu / samples.length : 0;
const memUsed = samples.reduce((sum, m) => sum + m.memory_usage_bytes, 0);
const memLimit = samples.reduce((sum, m) => sum + m.memory_limit_bytes, 0);
const memPct = memLimit > 0 ? (memUsed / memLimit) * 100 : 0;
const netRx = samples.reduce((sum, m) => sum + m.network_rx_bytes, 0);
const netTx = samples.reduce((sum, m) => sum + m.network_tx_bytes, 0);
const instanceCount = samples.reduce((sum, m) => sum + m.instances.length, 0);
// Rolling history for the sparklines — accumulated from real samples.
const [cpuHistory, setCpuHistory] = useState<number[]>([]);
const [netHistory, setNetHistory] = useState<{ rx: number[]; tx: number[] }>({ rx: [], tx: [] });
const lastNetRef = useRef<{ rx: number; tx: number; at: number } | null>(null);
const samplesKey = samples.map((s) => `${s.service_id}:${s.collected_at}`).join('|');
const avgCpuRef = avgCpu;
const netRxRef = netRx;
const netTxRef = netTx;
useEffect(() => { useEffect(() => {
if (!samplesKey) return; if (!isRunning) return;
const now = Date.now();
const prev = lastNetRef.current;
const elapsed = prev ? Math.max(1, (now - prev.at) / 1000) : 0;
const rxRate = prev && elapsed > 0 ? Math.max(0, (netRxRef - prev.rx) / elapsed) : 0;
const txRate = prev && elapsed > 0 ? Math.max(0, (netTxRef - prev.tx) / elapsed) : 0;
lastNetRef.current = { rx: netRxRef, tx: netTxRef, at: now };
setCpuHistory((h) => [...h, Math.min(100, avgCpuRef)].slice(-24));
setNetHistory((h) => ({ rx: [...h.rx, rxRate].slice(-24), tx: [...h.tx, txRate].slice(-24) }));
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on the samples' collected_at signature
}, [samplesKey]);
const cpuHealth = healthLabel(avgCpu); const interval = setInterval(() => {
const memHealth = healthLabel(memPct); setData((prev) => {
const reporting = samples.length; const newCpu = [...prev.cpu.slice(1), getRandom(10, 45)];
const noData = !isDemoMode && liveServices.length > 0 && reporting === 0; const newUsers = [...prev.users.slice(1), getRandom(60, 110)];
const newPerf = [...prev.performance.slice(1), getRandom(82, 99)];
const newPerfAlt = [...prev.performanceAlt.slice(1), getRandom(60, 85)];
return {
...prev,
cpu: newCpu,
ram: getRandom(50, 85),
ramUsed: `${(8 * (prev.ram / 100)).toFixed(1)} GB`,
users: newUsers,
performance: newPerf,
performanceAlt: newPerfAlt,
upSpeed: parseFloat((Math.random() * 5 + 8).toFixed(1)),
downSpeed: parseFloat((Math.random() * 3 + 4).toFixed(1)),
};
});
}, 2000);
return () => clearInterval(interval);
}, [isRunning]);
const currentCpu = data.cpu[data.cpu.length - 1];
const currentUsers = data.users[data.users.length - 1];
const currentPerf = data.performance[data.performance.length - 1];
return ( return (
<div> <div>
<div className="flex items-center justify-between mb-4"> {/* Metrics Header - self.html exact match */}
<span className="text-[15px] font-bold text-[var(--text-primary)]">Metrics</span> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<span className="text-xs text-[var(--text-muted)]"> <span style={{ fontSize: '15px', fontWeight: 700, color: '#e8e9f0' }}>Metrics</span>
{isDemoMode <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
? 'sample data' <button style={{
: reporting > 0 height: '32px',
? `${reporting} service${reporting === 1 ? '' : 's'} reporting · every 5s` padding: '0 12px',
: 'no running services'} borderRadius: '9px',
</span> border: '1px solid rgba(255,255,255,0.09)',
</div> background: 'rgba(255,255,255,0.04)',
color: '#9295a4',
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4"> fontSize: '12.5px',
{/* CPU */} fontWeight: 500,
<div style={cardStyle}> fontFamily: 'inherit',
<div className="flex items-center gap-2.5 mb-3"> cursor: 'pointer',
<span className="card-icon"><Cpu size={16} /></span> display: 'flex',
<span className="text-sm font-semibold text-[var(--text-primary)]">CPU Usage</span> alignItems: 'center',
gap: '6px',
}}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
</svg>
Filter
</button>
<div className="pill-group">
{(['day', 'month', 'year'] as const).map((range) => (
<div
key={range}
className={`pill ${timeRange === range ? 'active' : ''}`}
onClick={() => setTimeRange(range)}
>
{range.charAt(0).toUpperCase() + range.slice(1)}
</div>
))}
</div> </div>
<div className="text-4xl font-black tracking-tight text-[var(--text-primary)]">
{avgCpu.toFixed(1)}%
</div>
<div className="mt-1 text-xs text-[var(--text-tertiary)]">
<span style={{ color: cpuHealth.color, fontWeight: 700 }}>{cpuHealth.text}</span>
{' '}avg across running services
</div>
<div style={{ height: 64, marginTop: 12 }}>
{cpuHistory.length > 1 ? (
<LineMetricChart data={cpuHistory} color="var(--accent-primary)" fillOpacity={0.15} showArea height={64} />
) : (
<div className="h-full rounded bg-[var(--surface-muted)] opacity-40" />
)}
</div>
</div>
{/* Memory */}
<div style={cardStyle}>
<div className="flex items-center gap-2.5 mb-3">
<span className="card-icon"><HardDrive size={16} /></span>
<span className="text-sm font-semibold text-[var(--text-primary)]">Memory</span>
</div>
<div className="text-4xl font-black tracking-tight text-[var(--text-primary)]">
{memPct.toFixed(0)}%
</div>
<div className="mt-1 text-xs text-[var(--text-tertiary)]">
<span style={{ color: memHealth.color, fontWeight: 700 }}>{memHealth.text}</span>
{' '}{formatBytes(memUsed)} used
</div>
<div className="mt-2 flex justify-center">
<DonutChart percentage={memPct} size={110} thickness={12} />
</div>
<div className="mt-1 text-center text-xs text-[var(--text-tertiary)] mono">
{formatBytes(memUsed)} / {memLimit > 0 ? formatBytes(memLimit) : '∞'}
</div>
</div>
{/* Network */}
<div style={cardStyle}>
<div className="flex items-center gap-2.5 mb-3">
<span className="card-icon"><ArrowDownUp size={16} /></span>
<span className="text-sm font-semibold text-[var(--text-primary)]">Network</span>
</div>
<div className="text-4xl font-black tracking-tight text-[var(--text-primary)]">
{formatBytes(netRx)}
</div>
<div className="mt-1 text-xs text-[var(--text-tertiary)]">
<span style={{ color: 'var(--accent-primary)', fontWeight: 700 }}>RX</span>
{' '}total · TX {formatBytes(netTx)}
</div>
<div style={{ height: 64, marginTop: 12 }}>
{netHistory.rx.length > 1 ? (
<LineMetricChart data={netHistory.rx} color="#7ab8ff" fillOpacity={0.12} showArea height={64} />
) : (
<div className="h-full rounded bg-[var(--surface-muted)] opacity-40" />
)}
</div>
<div className="mt-2 text-[11px] text-[var(--text-muted)] mono">
{netHistory.rx.length > 0
? `↓ ${formatBytes(netHistory.rx[netHistory.rx.length - 1])}/s · ↑ ${formatBytes(netHistory.tx[netHistory.tx.length - 1])}/s`
: 'rate accumulating…'}
</div>
</div>
{/* Instances */}
<div style={cardStyle}>
<div className="flex items-center gap-2.5 mb-3">
<span className="card-icon"><Boxes size={16} /></span>
<span className="text-sm font-semibold text-[var(--text-primary)]">Instances</span>
</div>
<div className="text-4xl font-black tracking-tight text-[var(--text-primary)]">
{isDemoMode ? liveServices.length : instanceCount}
</div>
<div className="mt-1 text-xs text-[var(--text-tertiary)]">
<span style={{ color: 'var(--success, #5ee6a0)', fontWeight: 700 }}>Running</span>
{' '}containers across {liveServices.length} service{liveServices.length === 1 ? '' : 's'}
</div>
<ul className="mt-3 space-y-1.5">
{liveServices.slice(0, 4).map((service, i) => {
const m = samples.find((s) => s.service_id === service.id);
return (
<li key={service.id} className="flex items-center justify-between text-xs">
<span className="truncate text-[var(--text-secondary)]">
<span className="mr-1.5 inline-block h-1.5 w-1.5 rounded-full bg-[var(--success,#5ee6a0)] align-middle" />
{service.name}
</span>
<span className="mono text-[var(--text-muted)]">
{m ? `${m.cpu_percent.toFixed(1)}%` : i < 3 ? '—' : ''}
</span>
</li>
);
})}
{liveServices.length > 4 && (
<li className="text-[11px] text-[var(--text-muted)]">+{liveServices.length - 4} more</li>
)}
</ul>
</div> </div>
</div> </div>
{noData && ( {/* Row 1: CPU, RAM, Cache - self.html exact: 13px gap */}
<p className="mt-3 text-xs text-[var(--text-muted)]"> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1.95fr', gap: '13px', marginBottom: '13px' }}>
Metrics collector is not reporting yet — check that the node agent is online. {/* CPU Card */}
</p> <div className="card">
)} <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
<div className="card-icon"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg></div>
<span style={{ fontSize: '14px', fontWeight: 600, color: '#e8e9f0' }}>CPU Usage</span>
</div>
<div style={{ fontSize: '38px', fontWeight: 900, letterSpacing: '-1.5px', lineHeight: 1, color: '#e8e9f0' }}>{currentCpu}%</div>
<div style={{ fontSize: '12px', color: '#6b6e7d', marginTop: '4px' }}>
<span style={{ color: currentCpu < 50 ? '#5ee6a0' : currentCpu < 75 ? '#f2c94c' : '#b4e34a', fontWeight: 700 }}>
{currentCpu < 50 ? 'Good' : currentCpu < 75 ? 'Average' : 'High'}
</span>{' '}
Daily usage
</div>
<div style={{ height: 76, margin: '12px 0 6px' }}>
<LineMetricChart data={data.cpu} color="#b4e34a" height={76} />
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingTop: '4px' }}>
<span style={{ fontSize: '13px', color: '#6b6e7d', fontWeight: 500, cursor: 'pointer' }}>Details</span>
<div className="arrow-btn">
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</div>
{/* RAM Card */}
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
<div className="card-icon"><svg viewBox="0 0 24 24"><rect x="2" y="8" width="20" height="8" rx="2"/><path d="M6 8V6M10 8V6M14 8V6M18 8V6M6 16v2M18 16v2"/></svg></div>
<span style={{ fontSize: '14px', fontWeight: 600, color: '#e8e9f0' }}>RAM Usage</span>
</div>
<div style={{ fontSize: '38px', fontWeight: 900, letterSpacing: '-1.5px', lineHeight: 1, color: '#e8e9f0' }}>{data.ram}%</div>
<div style={{ fontSize: '12px', color: '#6b6e7d', marginTop: '4px' }}>
<span style={{ color: data.ram < 60 ? '#5ee6a0' : '#f2c94c', fontWeight: 700 }}>
{data.ram < 60 ? 'Good' : 'Average'}
</span>{' '}
Daily usage
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', margin: '10px 0 4px', position: 'relative' }}>
<DonutChart percent={data.ram} color="#f2c94c" size={160} thickness={16} />
<div style={{ position: 'absolute', bottom: 14, textAlign: 'center' }}>
<div style={{ fontSize: '10.5px', color: '#6b6e7d', marginBottom: 1 }}>Used</div>
<div style={{ fontSize: '12.5px', fontWeight: 700, color: '#e8e9f0' }}>{data.ramUsed} / 8GB</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingTop: '4px' }}>
<span style={{ fontSize: '13px', color: '#6b6e7d', fontWeight: 500, cursor: 'pointer' }}>Details</span>
<div className="arrow-btn">
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</div>
{/* Cache Card */}
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
<div className="card-icon"><svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></div>
<span style={{ fontSize: '14px', fontWeight: 600, color: '#e8e9f0' }}>Cache</span>
</div>
<div style={{ fontSize: '38px', fontWeight: 900, letterSpacing: '-1.5px', lineHeight: 1, color: '#e8e9f0' }}>{data.cache} MB</div>
<div style={{ fontSize: '12px', color: '#6b6e7d', marginTop: '4px' }}>
<span style={{ color: '#f2c94c', fontWeight: 700 }}>220MB Average</span>{' '}
cached images and files
</div>
{/* Segmented Bar */}
<div style={{ display: 'flex', alignItems: 'center', gap: '5px', margin: '16px 0 15px', height: '32px' }}>
<div className="cache-seg" style={{ width: '43%', background: '#ff6b5b', borderRadius: '10px 4px 4px 10px' }} />
<div className="cache-seg" style={{ width: '13%', background: '#8c6ef0', borderRadius: '5px' }} />
<div className="cache-seg" style={{ flex: 1, background: 'rgba(255,255,255,0.07)', borderRadius: '4px 10px 10px 4px' }} />
</div>
{/* Stats row */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr auto 1fr', gap: 0, alignItems: 'stretch' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '11px', color: '#6b6e7d', marginBottom: '5px' }}>
<div className="stat-dot" style={{ background: '#ff6b5b' }} />
Cache
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '4px' }}>
<span style={{ fontSize: '15px', fontWeight: 800, color: '#e8e9f0' }}>212 MB</span>
<span style={{ fontSize: '11px', color: '#6b6e7d' }}>12%</span>
</div>
</div>
<div style={{ width: '1px', background: 'rgba(255,255,255,0.08)', margin: '0 16px' }} />
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '11px', color: '#6b6e7d', marginBottom: '5px' }}>
<div className="stat-dot" style={{ background: '#8c6ef0' }} />
Non-Cache
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '4px' }}>
<span style={{ fontSize: '15px', fontWeight: 800, color: '#e8e9f0' }}>85.5 MB</span>
<span style={{ fontSize: '11px', color: '#6b6e7d' }}>4%</span>
</div>
</div>
<div style={{ width: '1px', background: 'rgba(255,255,255,0.08)', margin: '0 16px' }} />
<div>
<div style={{ fontSize: '11px', color: '#6b6e7d', marginBottom: '5px' }}>Total</div>
<div style={{ fontSize: '15px', fontWeight: 800, color: '#e8e9f0' }}>1.75 GB</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingTop: '16px' }}>
<span style={{ fontSize: '13px', color: '#6b6e7d', fontWeight: 500, cursor: 'pointer' }}>Details</span>
<div className="arrow-btn">
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</div>
</div>
{/* Row 2: Active Users, Performance - self.html exact: 13px gap */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '13px' }}>
{/* Active Users - horizontal layout */}
<div className="card" style={{ flexDirection: 'row', padding: 0, overflow: 'hidden' }}>
<div style={{ flex: 1, padding: '20px 18px 18px 20px', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
<div className="card-icon"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
<span style={{ fontSize: '14px', fontWeight: 600, color: '#e8e9f0' }}>Active User</span>
</div>
<div style={{ fontSize: '36px', fontWeight: 900, letterSpacing: '-1.5px', lineHeight: 1, color: '#e8e9f0' }}>{currentUsers} K</div>
<div style={{ fontSize: '12px', color: '#6b6e7d', marginTop: '4px' }}>User active right now</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px', marginTop: '12px', flexWrap: 'wrap' }}>
{['🇨🇳', '🇮🇩', '🇲🇲', '🇲🇾', '🇯🇵', '🇮🇳', '🇰🇷', '🇵🇭'].map((flag, i) => (
<span key={i} className="flag">{flag}</span>
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 'auto', paddingTop: '14px' }}>
<span style={{ fontSize: '13px', color: '#6b6e7d', fontWeight: 500, cursor: 'pointer' }}>Details</span>
<div className="arrow-btn">
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</div>
<div style={{ width: '50%', padding: '16px 14px 46px 0', display: 'flex', alignItems: 'flex-end' }}>
<LineMetricChart data={data.users} color="var(--accent-primary)" fillOpacity={0.15} showArea height={130} />
</div>
</div>
{/* Performance Card */}
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
<div className="card-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></div>
<span style={{ fontSize: '14px', fontWeight: 600, color: '#e8e9f0' }}>Performance</span>
</div>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '16px', flex: 1 }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '36px', fontWeight: 900, letterSpacing: '-1.5px', lineHeight: 1, color: '#e8e9f0' }}>{currentPerf}%</div>
<div style={{ fontSize: '12px', color: '#6b6e7d', marginTop: '4px' }}>
<span style={{ color: currentPerf > 85 ? '#5ee6a0' : '#f2c94c', fontWeight: 700 }}>
{currentPerf > 85 ? 'Good' : 'Average'}
</span>{' '}
Last scan
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '8px' }}>
<div style={{ width: 134, height: 58 }}>
<DualLineChart data1={data.performance} data2={data.performanceAlt} height={58} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'flex-end' }}>
<div className="speed-row" style={{ color: '#7ab8ff' }}>
<svg viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 19 19 12"/></svg>
<span>{data.upSpeed}</span> Mbps
</div>
<div className="speed-row" style={{ color: 'var(--accent-primary)' }}>
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 5 5 12"/></svg>
<span>{data.downSpeed}</span> Mbps
</div>
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingTop: '14px' }}>
<span style={{ fontSize: '13px', color: '#6b6e7d', fontWeight: 500, cursor: 'pointer' }}>Check Speed</span>
<div className="arrow-btn">
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</div>
</div>
</div> </div>
); );
} }
@@ -78,7 +78,7 @@ export function SharedVariablesSection({ projectId }: { projectId: string }) {
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{rows.map((row, i) => ( {rows.map((row, i) => (
<div key={i} className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_auto] gap-2 items-center"> <div key={i} className="grid grid-cols-[1fr_1fr_auto_auto] gap-2 items-center">
<input <input
value={row.key} value={row.key}
onChange={(e) => updateRow(i, { key: e.target.value })} onChange={(e) => updateRow(i, { key: e.target.value })}
@@ -765,7 +765,7 @@ export function ProjectWorkspacePage() {
</span> </span>
<button <button
onClick={() => setCommandPaletteOpen(true)} onClick={() => setCommandPaletteOpen(true)}
className="hidden lg:flex items-center gap-2 h-8 px-2.5 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-[var(--text-muted)] text-xs hover:border-[var(--border-default)] hover:text-[var(--text-secondary)] transition-colors" className="hidden md:flex items-center gap-2 h-8 px-2.5 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-[var(--text-muted)] text-xs hover:border-[var(--border-default)] hover:text-[var(--text-secondary)] transition-colors"
> >
<Search size={12} /> <Search size={12} />
<kbd className="px-1 py-0.5 rounded bg-[var(--surface-card)] text-[10px] font-mono">⌘K</kbd> <kbd className="px-1 py-0.5 rounded bg-[var(--surface-card)] text-[10px] font-mono">⌘K</kbd>
@@ -826,7 +826,7 @@ export function ProjectWorkspacePage() {
</div> </div>
</div> </div>
)} )}
<div className="w-full px-4 py-6 sm:px-8"> <div className="w-full px-8 py-6">
<section className="min-w-0"> <section className="min-w-0">
{activeView === 'observability' && ( {activeView === 'observability' && (
<div className="space-y-6"> <div className="space-y-6">
@@ -860,7 +860,11 @@ export function ProjectWorkspacePage() {
</div> </div>
{/* Metrics Dashboard */} {/* Metrics Dashboard */}
<MetricsDashboard services={visibleServices} isDemoMode={isDemoMode} /> <MetricsDashboard
isRunning={runningServices > 0}
onStop={() => toast.showToast('Services stopped', 'warning')}
onRestart={() => toast.showToast('Services restarted', 'success')}
/>
{/* Service List */} {/* Service List */}
<div className="panel p-6"> <div className="panel p-6">
+1 -2
View File
@@ -492,12 +492,11 @@ export function getApiBaseUrl(): string {
// pingServer hits the public /health route — any HTTP response proves // pingServer hits the public /health route — any HTTP response proves
// reachability, and no auth means no 401 noise in the connectivity probe. // reachability, and no auth means no 401 noise in the connectivity probe.
export async function pingServer(): Promise<boolean> { export async function pingServer(): Promise<void> {
const res = await fetch(`${API_ROOT}/health`); const res = await fetch(`${API_ROOT}/health`);
if (!res.ok) { if (!res.ok) {
throw new Error(`Server returned ${res.status}`); throw new Error(`Server returned ${res.status}`);
} }
return true;
} }
export type SetupStatus = { export type SetupStatus = {
-1
View File
@@ -23,7 +23,6 @@ const queryClient = new QueryClient({
// High-churn or bulky keys never hit localStorage — a stale log tail is // High-churn or bulky keys never hit localStorage — a stale log tail is
// worse than none. // worse than none.
const NEVER_PERSIST = new Set([ const NEVER_PERSIST = new Set([
'auth-session',
'shell-notifications', 'shell-notifications',
'notifications', 'notifications',
'service-logs', 'service-logs',
+1 -5
View File
@@ -59,12 +59,8 @@ export default defineConfig({
}, },
proxy: { proxy: {
// Same-origin API path for dev - matches the nginx proxy in the shipped image // Same-origin API path for dev - matches the nginx proxy in the shipped image
'/health': {
target: process.env.VITE_API_PROXY ?? 'http://localhost:8082',
changeOrigin: true,
},
'/api': { '/api': {
target: process.env.VITE_API_PROXY ?? 'http://localhost:8082', target: 'http://localhost:8082',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
}, },