Phase 1: config loading, database migrations, audit log

- internal/config: TOML config with env overrides (MCR_ prefix),
  required field validation, same-filesystem check, defaults
- internal/db: SQLite via modernc.org/sqlite, WAL mode, 2 migrations
  (core registry tables + policy/audit), foreign key cascades
- internal/db: audit log write/list with filtering and pagination
- deploy/examples/mcr.toml: annotated example configuration
- .golangci.yaml: disable fieldalignment (readability over micro-opt)
- checkpoint skill copied from mcias

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 13:14:19 -07:00
parent 369558132b
commit fde66be9c1
15 changed files with 1433 additions and 9 deletions

197
internal/db/db_test.go Normal file
View File

@@ -0,0 +1,197 @@
package db
import (
"path/filepath"
"testing"
)
func openTestDB(t *testing.T) *DB {
t.Helper()
path := filepath.Join(t.TempDir(), "test.db")
d, err := Open(path)
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { _ = d.Close() })
return d
}
func TestOpenAndMigrate(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
// Verify schema version.
ver, err := d.SchemaVersion()
if err != nil {
t.Fatalf("SchemaVersion: %v", err)
}
if ver != 2 {
t.Fatalf("schema version: got %d, want 2", ver)
}
}
func TestMigrateIdempotent(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate (first): %v", err)
}
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate (second): %v", err)
}
ver, err := d.SchemaVersion()
if err != nil {
t.Fatalf("SchemaVersion: %v", err)
}
if ver != 2 {
t.Fatalf("schema version: got %d, want 2", ver)
}
}
func TestTablesExist(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
tables := []string{
"schema_migrations",
"repositories",
"manifests",
"tags",
"blobs",
"manifest_blobs",
"uploads",
"policy_rules",
"audit_log",
}
for _, table := range tables {
var name string
err := d.QueryRow(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table,
).Scan(&name)
if err != nil {
t.Fatalf("table %q not found: %v", table, err)
}
}
}
func TestForeignKeyEnforcement(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
// Inserting a manifest with a nonexistent repository_id should fail.
_, err := d.Exec(`INSERT INTO manifests (repository_id, digest, media_type, content, size)
VALUES (9999, 'sha256:abc', 'application/json', '{}', 2)`)
if err == nil {
t.Fatal("expected foreign key violation, got nil")
}
}
func TestTagCascadeOnManifestDelete(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
// Create a repository, manifest, and tag.
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('testrepo')`)
if err != nil {
t.Fatalf("insert repo: %v", err)
}
_, err = d.Exec(`INSERT INTO manifests (repository_id, digest, media_type, content, size)
VALUES (1, 'sha256:abc123', 'application/vnd.oci.image.manifest.v1+json', '{}', 2)`)
if err != nil {
t.Fatalf("insert manifest: %v", err)
}
_, err = d.Exec(`INSERT INTO tags (repository_id, name, manifest_id) VALUES (1, 'latest', 1)`)
if err != nil {
t.Fatalf("insert tag: %v", err)
}
// Delete the manifest — tag should cascade.
_, err = d.Exec(`DELETE FROM manifests WHERE id = 1`)
if err != nil {
t.Fatalf("delete manifest: %v", err)
}
var count int
if err := d.QueryRow(`SELECT COUNT(*) FROM tags`).Scan(&count); err != nil {
t.Fatalf("count tags: %v", err)
}
if count != 0 {
t.Fatalf("tags after manifest delete: got %d, want 0", count)
}
}
func TestManifestBlobsCascadeOnManifestDelete(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('testrepo')`)
if err != nil {
t.Fatalf("insert repo: %v", err)
}
_, err = d.Exec(`INSERT INTO manifests (repository_id, digest, media_type, content, size)
VALUES (1, 'sha256:abc123', 'application/vnd.oci.image.manifest.v1+json', '{}', 2)`)
if err != nil {
t.Fatalf("insert manifest: %v", err)
}
_, err = d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:layer1', 1024)`)
if err != nil {
t.Fatalf("insert blob: %v", err)
}
_, err = d.Exec(`INSERT INTO manifest_blobs (manifest_id, blob_id) VALUES (1, 1)`)
if err != nil {
t.Fatalf("insert manifest_blobs: %v", err)
}
// Delete manifest — manifest_blobs should cascade, blob should remain.
_, err = d.Exec(`DELETE FROM manifests WHERE id = 1`)
if err != nil {
t.Fatalf("delete manifest: %v", err)
}
var mbCount int
if err := d.QueryRow(`SELECT COUNT(*) FROM manifest_blobs`).Scan(&mbCount); err != nil {
t.Fatalf("count manifest_blobs: %v", err)
}
if mbCount != 0 {
t.Fatalf("manifest_blobs after delete: got %d, want 0", mbCount)
}
// Blob row should still exist (GC handles file cleanup).
var blobCount int
if err := d.QueryRow(`SELECT COUNT(*) FROM blobs`).Scan(&blobCount); err != nil {
t.Fatalf("count blobs: %v", err)
}
if blobCount != 1 {
t.Fatalf("blobs after manifest delete: got %d, want 1", blobCount)
}
}
func TestWALMode(t *testing.T) {
d := openTestDB(t)
var mode string
if err := d.QueryRow(`PRAGMA journal_mode`).Scan(&mode); err != nil {
t.Fatalf("PRAGMA journal_mode: %v", err)
}
if mode != "wal" {
t.Fatalf("journal_mode: got %q, want %q", mode, "wal")
}
}