feat(core): add naming, RCON, store, config, and image-build libraries
Foundational libraries: deterministic resource naming, the RCON client, the Postgres store with embedded SQL migrations, configuration loading, and container image-build helpers.
This commit is contained in:
18 files changed
+3475
No files matched your search
@@ -0,0 +1,186 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PGStore is the production Store backed by Postgres (spec §6, §16). It writes
|
||||
// the two tables of the build subsystem — image_builds and image_whitelist —
|
||||
// and is the *only* component that holds database credentials: the build Pod
|
||||
// never does (the weak-SA red line). The SQL here is exercised by integration
|
||||
// tests against a live database, not the hermetic build_test.go suite. Every
|
||||
// statement is a narrow operation; there is no generic UPDATE escape hatch.
|
||||
type PGStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewPGStore wraps an existing pool (from store.PostgresDriver.DB()).
|
||||
func NewPGStore(db *sql.DB) *PGStore { return &PGStore{db: db} }
|
||||
|
||||
func (s *PGStore) CreateBuild(ctx context.Context, b *Build) error {
|
||||
const q = `INSERT INTO image_builds
|
||||
(id, image_ref, status, dockerfile, context_ref, base_image, requested_by, created_at)
|
||||
VALUES ($1, $2, $3, $4, NULLIF($5, ''), NULLIF($6, ''), $7, $8)`
|
||||
_, err := s.db.ExecContext(ctx, q,
|
||||
b.ID, b.ImageRef, string(b.Status), b.Dockerfile, b.ContextRef, b.BaseImage,
|
||||
b.RequestedBy, b.CreatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *PGStore) GetBuild(ctx context.Context, id string) (*Build, error) {
|
||||
const q = `SELECT id, image_ref, status, dockerfile, context_ref, base_image,
|
||||
requested_by, job_name, log_ref, error, created_at, finished_at
|
||||
FROM image_builds WHERE id = $1`
|
||||
return s.scanBuild(s.db.QueryRowContext(ctx, q, id))
|
||||
}
|
||||
|
||||
func (s *PGStore) scanBuild(row *sql.Row) (*Build, error) {
|
||||
var (
|
||||
b Build
|
||||
status string
|
||||
ctxRef, base, jobName, logRef, eMsg sql.NullString
|
||||
finished sql.NullTime
|
||||
)
|
||||
switch err := row.Scan(&b.ID, &b.ImageRef, &status, &b.Dockerfile, &ctxRef, &base,
|
||||
&b.RequestedBy, &jobName, &logRef, &eMsg, &b.CreatedAt, &finished); {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, ErrNotFound
|
||||
case err != nil:
|
||||
return nil, err
|
||||
}
|
||||
b.Status = Status(status)
|
||||
b.ContextRef = ctxRef.String
|
||||
b.BaseImage = base.String
|
||||
b.JobName = jobName.String
|
||||
b.LogRef = logRef.String
|
||||
b.Error = eMsg.String
|
||||
if finished.Valid {
|
||||
t := finished.Time
|
||||
b.FinishedAt = &t
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func (s *PGStore) SetBuildJob(ctx context.Context, id, jobName string) error {
|
||||
const q = `UPDATE image_builds SET job_name = $2, status = 'building'
|
||||
WHERE id = $1 AND status = 'pending'`
|
||||
res, err := s.db.ExecContext(ctx, q, id, jobName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PGStore) FinishBuild(ctx context.Context, id string, status Status, errMsg string, at time.Time) error {
|
||||
const q = `UPDATE image_builds SET status = $2, error = NULLIF($3, ''), finished_at = $4
|
||||
WHERE id = $1`
|
||||
res, err := s.db.ExecContext(ctx, q, id, string(status), errMsg, at)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PGStore) ListUnfinishedBuilds(ctx context.Context) ([]Build, error) {
|
||||
const q = `SELECT id, image_ref, status, dockerfile, context_ref, base_image,
|
||||
requested_by, job_name, log_ref, error, created_at, finished_at
|
||||
FROM image_builds WHERE status IN ('pending', 'building') ORDER BY created_at ASC`
|
||||
rows, err := s.db.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Build
|
||||
for rows.Next() {
|
||||
var (
|
||||
b Build
|
||||
status string
|
||||
ctxRef, base, jobName, logRef, eMsg sql.NullString
|
||||
finished sql.NullTime
|
||||
)
|
||||
if err := rows.Scan(&b.ID, &b.ImageRef, &status, &b.Dockerfile, &ctxRef, &base,
|
||||
&b.RequestedBy, &jobName, &logRef, &eMsg, &b.CreatedAt, &finished); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Status = Status(status)
|
||||
b.ContextRef = ctxRef.String
|
||||
b.BaseImage = base.String
|
||||
b.JobName = jobName.String
|
||||
b.LogRef = logRef.String
|
||||
b.Error = eMsg.String
|
||||
if finished.Valid {
|
||||
t := finished.Time
|
||||
b.FinishedAt = &t
|
||||
}
|
||||
out = append(out, b)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// AdmitBuiltImage upserts the whitelist row on scan-gate success. ON CONFLICT
|
||||
// re-enables and re-stamps a previously-removed or superseded ref, so a rebuild
|
||||
// of the same tag re-admits it (spec §16).
|
||||
func (s *PGStore) AdmitBuiltImage(ctx context.Context, img Image) error {
|
||||
const q = `INSERT INTO image_whitelist
|
||||
(image_ref, source, build_id, added_by, enabled, added_at)
|
||||
VALUES ($1, 'built', NULLIF($2, ''), $3, true, $4)
|
||||
ON CONFLICT (image_ref) DO UPDATE
|
||||
SET source = 'built', build_id = EXCLUDED.build_id, added_by = EXCLUDED.added_by,
|
||||
enabled = true, added_at = EXCLUDED.added_at`
|
||||
_, err := s.db.ExecContext(ctx, q, img.ImageRef, img.BuildID, img.AddedBy, img.AddedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *PGStore) ListImages(ctx context.Context) ([]Image, error) {
|
||||
const q = `SELECT image_ref, source, build_id, added_by, enabled, added_at
|
||||
FROM image_whitelist ORDER BY added_at DESC`
|
||||
rows, err := s.db.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Image
|
||||
for rows.Next() {
|
||||
var (
|
||||
img Image
|
||||
buildID sql.NullString
|
||||
)
|
||||
if err := rows.Scan(&img.ImageRef, &img.Source, &buildID, &img.AddedBy,
|
||||
&img.Enabled, &img.AddedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img.BuildID = buildID.String
|
||||
out = append(out, img)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *PGStore) AddExternalImage(ctx context.Context, img Image) error {
|
||||
const q = `INSERT INTO image_whitelist
|
||||
(image_ref, source, added_by, enabled, added_at)
|
||||
VALUES ($1, 'external', $2, true, $3)
|
||||
ON CONFLICT (image_ref) DO UPDATE
|
||||
SET source = 'external', build_id = NULL, added_by = EXCLUDED.added_by,
|
||||
enabled = true, added_at = EXCLUDED.added_at`
|
||||
_, err := s.db.ExecContext(ctx, q, img.ImageRef, img.AddedBy, img.AddedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *PGStore) RemoveImage(ctx context.Context, imageRef string) error {
|
||||
res, err := s.db.ExecContext(ctx, `DELETE FROM image_whitelist WHERE image_ref = $1`, imageRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user