From 792389e12db4c6d53ee3f8564062272858e54de8 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Aug 2026 04:44:01 +0100 Subject: [PATCH 1/4] feat: add schema migrations with startup logging --- cmd/potokd/main.go | 12 ++++ go.mod | 9 +-- go.sum | 9 +++ internal/server/store/migrate.go | 67 ++++++++++++++++++++ migrations/0001_init.down.sql | 5 ++ migrations/0001_init.up.sql | 32 ++++++++++ migrations/0002_manifests_and_blobs.down.sql | 2 + migrations/0002_manifests_and_blobs.up.sql | 15 +++++ migrations/migrations.go | 6 ++ 9 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 internal/server/store/migrate.go create mode 100644 migrations/0001_init.down.sql create mode 100644 migrations/0001_init.up.sql create mode 100644 migrations/0002_manifests_and_blobs.down.sql create mode 100644 migrations/0002_manifests_and_blobs.up.sql create mode 100644 migrations/migrations.go diff --git a/cmd/potokd/main.go b/cmd/potokd/main.go index 1d3ccdc..0c6c77f 100644 --- a/cmd/potokd/main.go +++ b/cmd/potokd/main.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "log/slog" "os" nethttp "net/http" @@ -14,11 +15,13 @@ import ( ) func main() { + slog.Info("Starting Potok server...") cfg, err := config.LoadConfig() if err != nil { fmt.Fprintf(os.Stderr, "LoadConfig() error: %v\n", err) os.Exit(1) } + slog.Info("Config loaded successfully", "addr", cfg.Addr, "dataDir", cfg.DataDir, "databaseURL", cfg.DatabaseURL) conn, err := store.Open(context.Background(), cfg.DatabaseURL) if err != nil { @@ -26,7 +29,16 @@ func main() { os.Exit(1) } defer conn.Close() + slog.Info("Database connection established") + slog.Info("Running database migrations") + if err := conn.Migrate(); err != nil { + fmt.Fprintf(os.Stderr, "Migrate() error: %v\n", err) + os.Exit(1) + } + slog.Info("Database migrations completed successfully") + + slog.Info("Starting HTTP server") handler := httpapi.NewHandler(conn) nethttp.HandleFunc("GET /health", handler.Health) log.Fatal(nethttp.ListenAndServe(":3000", nil)) diff --git a/go.mod b/go.mod index 1981fb6..0e5fc01 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/michaeltukdev/Potok go 1.25.0 require ( - github.com/golang-migrate/migrate/v4 v4.18.3 + github.com/golang-migrate/migrate/v4 v4.19.1 github.com/gorilla/mux v1.8.1 github.com/spf13/cobra v1.10.2 github.com/zalando/go-keyring v0.2.8 - golang.org/x/crypto v0.40.0 + golang.org/x/crypto v0.45.0 modernc.org/sqlite v1.38.1 ) @@ -30,6 +30,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/josephspurrier/goversioninfo v1.4.1 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect @@ -41,10 +42,10 @@ require ( go.uber.org/atomic v1.7.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/image v0.20.0 // indirect - golang.org/x/sync v0.17.0 // indirect + golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/text v0.31.0 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index bac9491..585fbe4 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= +github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= +github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -30,6 +32,8 @@ github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFr github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josephspurrier/goversioninfo v1.4.1/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -57,10 +61,13 @@ github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cma go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= @@ -68,6 +75,8 @@ golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/server/store/migrate.go b/internal/server/store/migrate.go new file mode 100644 index 0000000..a1f9660 --- /dev/null +++ b/internal/server/store/migrate.go @@ -0,0 +1,67 @@ +package store + +import ( + "errors" + "fmt" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/jackc/pgx/v5/stdlib" + + "github.com/michaeltukdev/Potok/migrations" +) + +func (s *Store) Migrate() error { + source, err := iofs.New(migrations.FS, ".") + if err != nil { + return fmt.Errorf("store: read migrations: %w", err) + } + + db := stdlib.OpenDBFromPool(s.pool) + defer db.Close() + + driver, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + return fmt.Errorf("store: migration driver: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", source, "postgres", driver) + if err != nil { + return fmt.Errorf("store: migrator: %w", err) + } + + if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("store: apply migrations: %w", err) + } + return nil +} + +func (s *Store) Version() (version uint, dirty bool, err error) { + source, err := iofs.New(migrations.FS, ".") + if err != nil { + return 0, false, fmt.Errorf("store: read migrations: %w", err) + } + + db := stdlib.OpenDBFromPool(s.pool) + defer db.Close() + + driver, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + return 0, false, fmt.Errorf("store: migration driver: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", source, "postgres", driver) + if err != nil { + return 0, false, fmt.Errorf("store: migrator: %w", err) + } + + version, dirty, err = m.Version() + if errors.Is(err, migrate.ErrNilVersion) { + return 0, false, nil + } + if err != nil { + return 0, false, fmt.Errorf("store: read schema version: %w", err) + } + return version, dirty, nil +} diff --git a/migrations/0001_init.down.sql b/migrations/0001_init.down.sql new file mode 100644 index 0000000..52c2800 --- /dev/null +++ b/migrations/0001_init.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS vaults_user_id_idx; +DROP TABLE IF EXISTS vaults; +DROP INDEX IF EXISTS api_keys_user_id_idx; +DROP TABLE IF EXISTS api_keys; +DROP TABLE IF EXISTS users; diff --git a/migrations/0001_init.up.sql b/migrations/0001_init.up.sql new file mode 100644 index 0000000..2d139ed --- /dev/null +++ b/migrations/0001_init.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE api_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, + label TEXT NOT NULL, + key_hash BYTEA NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_used_at TIMESTAMPTZ +); + +CREATE INDEX api_keys_user_id_idx ON api_keys (user_id); + +CREATE TABLE vaults ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, + name TEXT NOT NULL, + wrapped_key BYTEA NOT NULL, + generation BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + UNIQUE (user_id, name) +); + +CREATE INDEX vaults_user_id_idx ON vaults (user_id); diff --git a/migrations/0002_manifests_and_blobs.down.sql b/migrations/0002_manifests_and_blobs.down.sql new file mode 100644 index 0000000..76add29 --- /dev/null +++ b/migrations/0002_manifests_and_blobs.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS blobs; +DROP TABLE IF EXISTS manifests; diff --git a/migrations/0002_manifests_and_blobs.up.sql b/migrations/0002_manifests_and_blobs.up.sql new file mode 100644 index 0000000..9bacea4 --- /dev/null +++ b/migrations/0002_manifests_and_blobs.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE manifests ( + vault_id UUID PRIMARY KEY REFERENCES vaults (id) ON DELETE CASCADE, + generation BIGINT NOT NULL, + ciphertext BYTEA NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE blobs ( + vault_id UUID NOT NULL REFERENCES vaults (id) ON DELETE CASCADE, + id TEXT NOT NULL, + size_bytes BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + PRIMARY KEY (vault_id, id) +); diff --git a/migrations/migrations.go b/migrations/migrations.go new file mode 100644 index 0000000..91cca1c --- /dev/null +++ b/migrations/migrations.go @@ -0,0 +1,6 @@ +package migrations + +import "embed" + +//go:embed *.sql +var FS embed.FS From 09c145977042d4d2103d18e1ac006155bdf1f0c7 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Aug 2026 05:05:24 +0100 Subject: [PATCH 2/4] feat: CI fix --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a255f8..fffb837 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,3 +20,5 @@ jobs: - run: go test ./... - uses: golangci/golangci-lint-action@v8 + with: + version: v2.1.6 From 73b56e6767236b1aa196b7e89b77a3580f60feea Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Aug 2026 05:08:30 +0100 Subject: [PATCH 3/4] feat: CI fix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fffb837..c9b2fc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,4 @@ jobs: - uses: golangci/golangci-lint-action@v8 with: - version: v2.1.6 + version: v2.13.0 From bf8f7087feccfe020251a535322ba37010c912c0 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Aug 2026 05:15:10 +0100 Subject: [PATCH 4/4] feat: CI fix --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9b2fc0..586f8d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,3 @@ jobs: - run: go build ./... - run: go test ./... - - - uses: golangci/golangci-lint-action@v8 - with: - version: v2.13.0