From afa582601425b5e36f4d3768a27a7e0d8ebb0506 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Mon, 12 Oct 2026 01:47:44 -0400 Subject: [PATCH 1/5] feat(compose): Hasura v2.51 as the v1.5 default with upgrade smoke (P7-LIVE-23) The image lock gains hasura (graphql-engine v2.51.0) and hasura-v14 (v2.44.0). v1.4 renders hasura-v14, v1.5 hasura (HasuraEntry, one compat.V15 gate). An explicit HASURA_VERSION wins, a project lock pin is kept, and the update plan offers the entry of the mode and never moves a newer pin back. scripts/ci/hasura-upgrade-smoke.sh proves the upgrade on Linux with data, metadata, an event trigger, a remote schema and a subscription, plus both rollback paths. --- .github/wiki/Compat-V15.md | 1 + .github/wiki/Image-Lock.md | 32 + cmd/commands/update_plan_hasura.go | 72 +++ cmd/commands/update_plan_hasura_test.go | 56 ++ cmd/commands/update_plan_sources.go | 4 + internal/build/lockfile_test.go | 2 +- internal/compose/core_services.go | 42 +- internal/compose/hasura_image_test.go | 150 +++++ internal/compose/image_inputs.go | 13 +- internal/compose/images.lock.json | 17 +- internal/compose/images.yaml | 5 +- internal/compose/images_lock_test.go | 5 +- internal/compose/service_catalog.go | 3 + .../fixtures/dev-minimal/golden-tree.txt | 4 +- scripts/ci/hasura-upgrade-smoke.sh | 570 ++++++++++++++++++ .../ci/testdata/hasura-smoke/metadata.json | 64 ++ .../migrations/20260101000000_smoke_items.sql | 20 + scripts/ci/testdata/hasura-smoke/receiver.py | 79 +++ scripts/ci/testdata/hasura-smoke/ws_sub.py | 128 ++++ 19 files changed, 1258 insertions(+), 9 deletions(-) create mode 100644 cmd/commands/update_plan_hasura.go create mode 100644 cmd/commands/update_plan_hasura_test.go create mode 100644 internal/compose/hasura_image_test.go create mode 100644 scripts/ci/hasura-upgrade-smoke.sh create mode 100644 scripts/ci/testdata/hasura-smoke/metadata.json create mode 100644 scripts/ci/testdata/hasura-smoke/migrations/20260101000000_smoke_items.sql create mode 100644 scripts/ci/testdata/hasura-smoke/receiver.py create mode 100644 scripts/ci/testdata/hasura-smoke/ws_sub.py diff --git a/.github/wiki/Compat-V15.md b/.github/wiki/Compat-V15.md index c02590350..bd7542f68 100644 --- a/.github/wiki/Compat-V15.md +++ b/.github/wiki/Compat-V15.md @@ -98,6 +98,7 @@ The table is generated. Never hand-edit it; run `compat-markers.sh --write-wiki` | P7-LIVE-20 | auth runs nhost/auth:0.36.0 (auth-v14) | the current nhost/auth release (auth) when AUTH_ENCRYPTION_KEY is set | `internal/compose/auth_image.go` | | P7-LIVE-22 | E151 for every ACME failure (exit of the failed process, else 2) | E470/E471 (exit 2) or E472 (exit 1) by failure class | `cmd/commands/ssl_renewal.go` | | P7-LIVE-22 | certbot is the default certificate path | the ACME engine is the default | `cmd/commands/ssl_renewal.go` | +| P7-LIVE-23 | hasura runs graphql-engine v2.44.0 (hasura-v14) | v2.51.0 (hasura) | `internal/compose/core_services.go` | | P7-PLUG-01 | v1 plugin.json read silently | one deprecation line per process on stderr | `internal/plugin/manifestv2/warn.go` | | P7-PLUG-11 | tier pro | license licensed | `cmd/commands/plugin_marketplace_cmds.go` | | P7-PLUG-11 | tier pro | license licensed | `cmd/commands/plugin_query.go` | diff --git a/.github/wiki/Image-Lock.md b/.github/wiki/Image-Lock.md index 612fd1870..753d92ca9 100644 --- a/.github/wiki/Image-Lock.md +++ b/.github/wiki/Image-Lock.md @@ -93,6 +93,38 @@ Auth migrates its schema one way, so rolling back means restoring the backup fro Pinning back without the restore is unsupported: 0.36.0 starts on the migrated schema, but every TOTP sign-in fails because the secrets are now encrypted. `scripts/ci/auth-smoke.sh` proves the upgrade and both rollback paths on Linux with users, sessions, a refresh token and a TOTP user (`NSELF_V15=1 bash scripts/ci/auth-smoke.sh`; it needs Docker, Go, curl, python3 and free ports 80, 443, 4000, 5432 and 8080). +## Hasura: graphql-engine v2.51 + +The lock carries two Hasura entries on `docker.io/hasura/graphql-engine`: `hasura-v14` (`v2.44.0`) and `hasura` (`v2.51.0`, the current v2 release). + +| Project | Hasura image | +|---|---| +| v1.4 | `hasura-v14`: `docker.io/hasura/graphql-engine:v2.44.0@sha256:...` in lock mode, `hasura/graphql-engine:v2.44.0` in legacy mode | +| v1.5, no lock pin yet (a new project) | `hasura` (v2.51.0) | +| the project lock pins v2.44.0 | the pin, in both modes, until `nself update` moves it | +| `HASURA_VERSION=v2.44.0` (what `nself init` writes into `.env`) or blank | the mode default, or the pin if there is one: a default, not an override | +| `HASURA_VERSION=v2.51.0` | `hasura` in any mode; it is an override, so it drops a lock pin of v2.44.0 with one notice | +| any other `HASURA_VERSION` (`v2.45.0`) | `hasura/graphql-engine:`, unpinned, in any mode | + +Hasura v2.51.0 does not change the metadata format or the migration contract. `scripts/ci/hasura-upgrade-smoke.sh` starts a project on v2.44.0 with a migrated fixture (tables and rows) and metadata (tracked tables, a relationship, a select permission, an event trigger to a webhook, a remote schema), takes a verified backup, switches the pin to v2.51.0 and checks that the rows are readable, that the metadata export is identical before and after (the `nself db hasura metadata export` tree and the normalised API JSON), that apply then export round-trips, and that a mutation, a subscription event, a remote schema call and an event trigger delivery work. The metadata catalog version is 48 on both releases. + +### Upgrade an existing project + +1. Take a verified backup: `nself backup create`, then `nself backup verify --restore-test`. Copy `nself.lock.yaml` aside too: it pins v2.44.0. +2. Move the pin with `nself update` (`nself update --plan` shows the move with a note, `--yes` applies it), or set `HASURA_VERSION=v2.51.0` for a project without a lock. In v1.4 `nself update` keeps v2.44.0 and never moves a newer pin back. +3. `nself build`, then `nself start`. Hasura applies its catalog migrations on that first start. + +### Roll back + +Treat the catalog migrations as one way: a Hasura release that raises the catalog version may leave an older engine unable to use the database (Hasura ships its own downgrade command for that). Rolling back means restoring the backup from step 1: + +1. `nself stop hasura`. +2. `nself backup restore --only pg --yes`. If it fails on dependencies of the newer catalog (E207), run `nself db reset --yes` first and repeat the restore (debt D-0663). +3. Put the saved `nself.lock.yaml` back (the v2.44.0 pin), or remove `HASURA_VERSION=v2.51.0` from the env. +4. `nself build`, then `nself start`. + +Pinning back without the restore is unsupported: the CLI does not check the catalog version, so a future pair of releases may refuse to start. For this pair the smoke observed that v2.44.0 starts on a v2.51.0 database (catalog version 48 on both) and passes every functional check, but rows written on v2.51.0 stay in the database and nothing restores the pre-switch state. The smoke proves both paths on Linux (`NSELF_V15=1 bash scripts/ci/hasura-upgrade-smoke.sh`; without `NSELF_V15` it runs the same functional checks on v2.44.0 as the baseline; it needs Docker, curl, python3, openssl and free ports 80, 443, 5432 and 8080). + ## How pins move 1. Edit the entry's `version` in `internal/compose/images.yaml` (a version bump is its own reviewed change). Leave `legacy_ref` alone. The weekly bump (`tools/imagebump`, `.github/workflows/image-pin-bump.yml`) does the same. diff --git a/cmd/commands/update_plan_hasura.go b/cmd/commands/update_plan_hasura.go new file mode 100644 index 000000000..42ac6da34 --- /dev/null +++ b/cmd/commands/update_plan_hasura.go @@ -0,0 +1,72 @@ +package commands + +// Purpose: the hasura row of `nself update` (P7-LIVE-23). The hasura service has +// two image lock entries on hasura/graphql-engine (hasura-v14 = v2.44.0, hasura = +// v2.51.0, which migrates the metadata catalog one way), so the generic +// per-repository lookup cannot choose between them. +// Inputs: the project's hasura pin. +// Outputs: the plan candidate for the hasura row. +// Constraints: the entry is compose.HasuraEntry() (v1.4 stays on v2.44.0); a pin +// newer than the entry is never moved back (that needs the pre-upgrade backup +// restored); the move to v2.51.0 carries a one-way note. + +import ( + "fmt" + "strings" + + "github.com/nself-org/cli/internal/compose" + "github.com/nself-org/cli/internal/upgrade/plan" +) + +// hasuraCandidate is the hasura row: the entry compose.HasuraEntry picks for the mode. +func hasuraCandidate(pin string) plan.Candidate { + r, ok := compose.LockedRef(compose.HasuraEntry()) + cur, _ := compose.LockedRef("hasura") + if !ok { + return plan.Candidate{Reason: plan.ReasonNoImage} + } + switch cmp := hasuraPinOrder(pin, r, cur); { + case cmp > 0: + return plan.Candidate{Reason: fmt.Sprintf("the pinned Hasura is newer than the %s this mode offers and has migrated the metadata catalog one way; going back needs the pre-upgrade backup restored (Image-Lock wiki, Roll back)", r.Version)} + case cmp < 0 && r.IndexDigest == cur.IndexDigest: + return plan.Candidate{Value: r.String(), Note: "Hasura " + r.Version + " migrates the metadata catalog one way on first start: take a verified backup first"} + } + return plan.Candidate{Value: r.String()} +} + +// hasuraPinOrder compares a hasura pin with the candidate entry: >0 when the pin is a +// newer release, <0 when older, 0 when equal or when the lock cannot tell (a digest the +// lock does not know). The digest of the current lock entry is a release the lock names. +func hasuraPinOrder(pin string, cand, cur compose.Ref) int { + base, digest, _ := strings.Cut(pin, "@") + if i := strings.LastIndex(base, ":"); i > strings.LastIndex(base, "/") { + if c, ok := compareHasuraVersion(base[i+1:], cand.Version); ok { + return c + } + } + if digest != "" && digest == cur.IndexDigest { + c, _ := compareHasuraVersion(cur.Version, cand.Version) + return c + } + return 0 +} + +// compareHasuraVersion orders two vMAJOR.MINOR.PATCH tags; ok is false for anything else. +func compareHasuraVersion(a, b string) (int, bool) { + var x, y [3]int + if _, err := fmt.Sscanf(a, "v%d.%d.%d", &x[0], &x[1], &x[2]); err != nil { + return 0, false + } + if _, err := fmt.Sscanf(b, "v%d.%d.%d", &y[0], &y[1], &y[2]); err != nil { + return 0, false + } + for i := range x { + if x[i] != y[i] { + if x[i] > y[i] { + return 1, true + } + return -1, true + } + } + return 0, true +} diff --git a/cmd/commands/update_plan_hasura_test.go b/cmd/commands/update_plan_hasura_test.go new file mode 100644 index 000000000..a27ca2bd4 --- /dev/null +++ b/cmd/commands/update_plan_hasura_test.go @@ -0,0 +1,56 @@ +package commands + +// update_plan_hasura_test.go — the hasura row of `nself update --plan` follows +// compose.HasuraEntry (P7-LIVE-23): v1.4 stays on v2.44.0, v1.5 offers v2.51.0 +// with a one-way note, and a pin newer than the offer is never moved back. + +import ( + "strings" + "testing" + + "github.com/nself-org/cli/internal/compat/compattest" + "github.com/nself-org/cli/internal/compose" +) + +func TestUpdatePlanHasuraRow(t *testing.T) { + v14, ok1 := compose.LockedRef("hasura-v14") + cur, ok2 := compose.LockedRef("hasura") + if !ok1 || !ok2 { + t.Fatal("image lock needs hasura and hasura-v14") + } + older := "hasura/graphql-engine:v2.43.0" + newer := "docker.io/hasura/graphql-engine:v2.99.0@sha256:" + strings.Repeat("ab", 32) + for _, tc := range []struct { + name string + v15 bool + pin string + want, note string // want "" = no value; the reason must then contain the note text + }{ + {"v1.4 keeps v2.44.0", false, v14.String(), v14.String(), ""}, + {"v1.4 moves an older pin to v2.44.0 without a note", false, older, v14.String(), ""}, + {"v1.4 never moves a v2.51.0 pin back", false, cur.String(), "", "backup restored"}, + {"v1.4 never moves a digest-only v2.51.0 pin back", false, "docker.io/hasura/graphql-engine@" + cur.IndexDigest, "", "backup restored"}, + {"v1.5 moves v2.44.0 to v2.51.0 with a one-way note", true, v14.String(), cur.String(), "one way"}, + {"v1.5 moves an older tag with a note", true, older, cur.String(), "one way"}, + {"v1.5 keeps v2.51.0 without a note", true, cur.String(), cur.String(), ""}, + {"v1.5 never moves a newer pin back", true, newer, "", "backup restored"}, + {"unknown digest-only pin moves to the offer", true, "docker.io/hasura/graphql-engine@sha256:" + strings.Repeat("cd", 32), cur.String(), ""}, + } { + t.Run(tc.name, func(t *testing.T) { + compattest.Set(t, tc.v15) + c := hasuraCandidate(tc.pin) + if tc.want == "" { + if c.Value != "" || !strings.Contains(c.Reason, tc.note) { + t.Fatalf("candidate = %+v, want no value and a reason containing %q", c, tc.note) + } + return + } + if c.Value != tc.want || c.Reason != "" { + t.Fatalf("candidate = %+v, want value %q", c, tc.want) + } + if tc.note == "" && c.Note != "" || tc.note != "" && !strings.Contains(c.Note, tc.note) { + t.Errorf("note = %q, want it to contain %q", c.Note, tc.note) + } + }) + } +} diff --git a/cmd/commands/update_plan_sources.go b/cmd/commands/update_plan_sources.go index efb5ff832..78e3edbaa 100644 --- a/cmd/commands/update_plan_sources.go +++ b/cmd/commands/update_plan_sources.go @@ -54,6 +54,10 @@ func gatherPlan(ctx context.Context, d updateDeps, l lock.Lock, target string) ( c.Images[k] = authCandidate(pin, passthrough) continue } + if k == "hasura" { // two lock entries, chosen by mode (P7-LIVE-23) + c.Images[k] = hasuraCandidate(pin) + continue + } if cand, ok := imageCandidate(k, pin, engine); ok { c.Images[k] = cand } diff --git a/internal/build/lockfile_test.go b/internal/build/lockfile_test.go index 60334784c..99002ea7b 100644 --- a/internal/build/lockfile_test.go +++ b/internal/build/lockfile_test.go @@ -249,7 +249,7 @@ func TestLockV14OptIn(t *testing.T) { } // The earlier v1.4 build left a tag-only compose; the first lock takes its // pins from it, with the digest from the CLI image lock (the versions match). - hasura, _ := compose.LockedRef("hasura") + hasura, _ := compose.LockedRef("hasura-v14") // what v1.4 runs (P7-LIVE-23) if l := readLock(t, f); l.Images["hasura"] != hasura.String() { t.Fatalf("first v1.4 lock pins hasura %q, want the CLI image lock's %q: %+v", l.Images["hasura"], hasura.String(), l) } diff --git a/internal/compose/core_services.go b/internal/compose/core_services.go index 5eef35455..f8ff90b4d 100644 --- a/internal/compose/core_services.go +++ b/internal/compose/core_services.go @@ -5,6 +5,7 @@ import ( "sort" "strings" + "github.com/nself-org/cli/internal/compat" "github.com/nself-org/cli/internal/config" ) @@ -169,7 +170,7 @@ func (g *Generator) buildHasuraService() (ServiceConfig, error) { } return ServiceConfig{ - Image: ResolveImage("hasura", ImageRef("hasura", cfg.Hasura.Version)), + Image: hasuraImage(cfg.Hasura.Version), ContainerName: fmt.Sprintf("%s_hasura", cfg.ProjectName), Restart: "unless-stopped", User: "1001:1001", @@ -198,3 +199,42 @@ func (g *Generator) buildHasuraService() (ServiceConfig, error) { }, }, nil } + +// HasuraEntry names the image lock entry the hasura service runs by default +// (EPIC D17, P7-LIVE-23): hasura-v14 (graphql-engine v2.44.0) in v1.4, hasura +// (the current v2.51.0) in v1.5. The update plan asks it too. +// +// The v2.51 catalog migration is one way: a database it has started on cannot +// go back to v2.44.0 without restoring the backup taken before the switch +// (Image-Lock wiki page, Roll back). +func HasuraEntry() string { + // compat.V15(P7-LIVE-23): hasura runs graphql-engine v2.44.0 (hasura-v14) -> v2.51.0 (hasura) + if compat.V15() { + return "hasura" + } + return "hasura-v14" +} + +// hasuraImage renders the hasura image for the config's HASURA_VERSION. Blank +// or v2.44.0 (the config default nself init writes into .env) is no override +// and renders the HasuraEntry entry; a version that is the other entry's +// release renders that entry in any mode. Neither is an explicit override +// (Ref.IsDefaultVersion), so a project lock pin wins over both, and a v1.5 +// project pinned to v2.44.0 stays on it until `nself update`. Any other value +// is an explicit tag and is used as given, unpinned. +func hasuraImage(version string) string { + version = strings.TrimSpace(version) + entry := HasuraEntry() + v14, _ := LockedRef("hasura-v14") + cur, _ := LockedRef("hasura") + switch { + case v14.IsDefaultVersion(version): + version = "" + case cur.IsDefaultVersion(version): + entry, version = "hasura", "" + } + if entry == "hasura" { + return ResolveImage("hasura", ImageRef("hasura", version)) + } + return ResolveImage("hasura", ImageRef("hasura-v14", version)) +} diff --git a/internal/compose/hasura_image_test.go b/internal/compose/hasura_image_test.go new file mode 100644 index 000000000..3ea891cb1 --- /dev/null +++ b/internal/compose/hasura_image_test.go @@ -0,0 +1,150 @@ +package compose + +// hasura_image_test.go — the hasura image per compat mode (EPIC D17, P7-LIVE-23). +// +// Purpose: prove that v1.4 runs graphql-engine v2.44.0 (hasura-v14), that v1.5 +// runs v2.51.0 (hasura), that an explicit HASURA_VERSION wins, and that a +// project lock pin of v2.44.0 survives v1.5 (the pin is what keeps an existing +// project on the version its database was migrated by). +// Inputs: minimalConfig plus the embedded image lock; digests are the lock's. +// Outputs: test failures naming mode, case and image. +// Constraints: no network, no Docker. + +import ( + "bytes" + "testing" + + "github.com/nself-org/cli/internal/compat" + "github.com/nself-org/cli/internal/compat/compattest" +) + +func hasuraImageFor(t *testing.T, version string) string { + t.Helper() + cfg := minimalConfig() + cfg.Hasura.Version = version + svc, err := NewGenerator(cfg).buildHasuraService() + if err != nil { + t.Fatalf("buildHasuraService: %v", err) + } + return svc.Image +} + +func TestHasuraImageByMode(t *testing.T) { + v14, ok1 := LockedRef("hasura-v14") + cur, ok2 := LockedRef("hasura") + if !ok1 || !ok2 { + t.Fatal("the image lock lacks hasura or hasura-v14") + } + if v14.Version != "v2.44.0" || cur.Version != "v2.51.0" || v14.Repository != cur.Repository || v14.IndexDigest == cur.IndexDigest { + t.Fatalf("lock entries hasura-v14 = %s, hasura = %s: want v2.44.0 and v2.51.0 of one repository with distinct digests", v14.String(), cur.String()) + } + restore := SetProjectPins(nil) + defer restore() + cases := []struct { + name, version string + v15 bool + mode string // pinning mode + want string + }{ + {"v1.4 lock default", "v2.44.0", false, PinningLock, v14.String()}, + {"v1.4 lock blank version", "", false, PinningLock, v14.String()}, + {"v1.4 legacy default", "v2.44.0", false, PinningLegacy, "hasura/graphql-engine:v2.44.0"}, + {"v1.5 lock default", "v2.44.0", true, PinningLock, cur.String()}, // the config default nself init writes + {"v1.5 lock blank version", "", true, PinningLock, cur.String()}, + {"v1.5 legacy default", "v2.44.0", true, PinningLegacy, "hasura/graphql-engine:v2.51.0"}, + {"explicit current release in v1.4", "v2.51.0", false, PinningLock, cur.String()}, + {"explicit current release in v1.4 legacy", "v2.51.0", false, PinningLegacy, "hasura/graphql-engine:v2.51.0"}, + {"explicit older release in v1.4", "v2.43.0", false, PinningLock, "hasura/graphql-engine:v2.43.0"}, + {"explicit older release in v1.5", "v2.43.0", true, PinningLock, "hasura/graphql-engine:v2.43.0"}, + {"explicit newer release in v1.5", "v2.52.0", true, PinningLock, "hasura/graphql-engine:v2.52.0"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + compattest.Set(t, tc.v15) + setMode(t, tc.mode) + if got := hasuraImageFor(t, tc.version); got != tc.want { + t.Errorf("hasura image = %q, want %q", got, tc.want) + } + }) + } + t.Run("HasuraEntry follows the mode", func(t *testing.T) { + compattest.Both(t, func(t *testing.T) { + want := "hasura-v14" + if compat.V15() { + want = "hasura" + } + if got := HasuraEntry(); got != want { + t.Errorf("HasuraEntry() = %q, want %q", got, want) + } + }) + }) +} + +func TestHasuraLockPinKept(t *testing.T) { + v14, _ := LockedRef("hasura-v14") + cur, _ := LockedRef("hasura") + pin := v14.String() // an existing project: its lock pins v2.44.0 + compattest.Both(t, func(t *testing.T) { + setMode(t, PinningLock) + var notices bytes.Buffer + defer PinNoticeSink(¬ices)() + defer SetProjectPins(map[string]string{"hasura": pin})() + // What nself init writes into .env (v2.44.0) and a blank value are CLI defaults and + // not overrides: the pin stays. + for _, v := range []string{"", v14.Version} { + t.Setenv("HASURA_VERSION", v) + if got := hasuraImageFor(t, v); got != pin { + t.Errorf("HASURA_VERSION=%q: hasura image = %q, want the pin %q", v, got, pin) + } + } + if notices.Len() != 0 { + t.Errorf("a kept pin printed a notice: %q", notices.String()) + } + // Any other version is explicit and wins over the pin, with one notice: the + // current release (init never writes it) and a tag the lock does not know. + for v, want := range map[string]string{cur.Version: cur.String(), "v2.45.0": "hasura/graphql-engine:v2.45.0"} { + notices.Reset() + resetPinNotices(t) + t.Setenv("HASURA_VERSION", v) + if got := hasuraImageFor(t, v); got != want { + t.Errorf("explicit HASURA_VERSION=%s: hasura image = %q, want %q", v, got, want) + } + if notices.Len() == 0 { + t.Errorf("HASURA_VERSION=%s dropped the pin without a notice", v) + } + } + }) + t.Run("no pin renders the mode default", func(t *testing.T) { + setMode(t, PinningLock) + defer SetProjectPins(nil)() + compattest.Set(t, false) + if got := hasuraImageFor(t, "v2.44.0"); got != v14.String() { + t.Errorf("v1.4 without a pin: %q, want %q", got, v14.String()) + } + compattest.Set(t, true) + if got := hasuraImageFor(t, "v2.44.0"); got != cur.String() { + t.Errorf("v1.5 without a pin: %q, want %q", got, cur.String()) + } + }) +} + +// TestHasuraLockPinKeptAfterUpdate: a project `nself update` moved to v2.51.0 keeps +// that pin whether .env still holds the init default or names the release. +func TestHasuraLockPinKeptAfterUpdate(t *testing.T) { + cur, _ := LockedRef("hasura") + compattest.Both(t, func(t *testing.T) { + setMode(t, PinningLock) + var notices bytes.Buffer + defer PinNoticeSink(¬ices)() + defer SetProjectPins(map[string]string{"hasura": cur.String()})() + for _, v := range []string{"", "v2.44.0", cur.Version} { + t.Setenv("HASURA_VERSION", v) + if got := hasuraImageFor(t, v); got != cur.String() { + t.Errorf("HASURA_VERSION=%q: hasura image = %q, want the pin %q", v, got, cur.String()) + } + } + if notices.Len() != 0 { + t.Errorf("a kept pin printed a notice: %q", notices.String()) + } + }) +} diff --git a/internal/compose/image_inputs.go b/internal/compose/image_inputs.go index 8333dbae5..2c409b0d8 100644 --- a/internal/compose/image_inputs.go +++ b/internal/compose/image_inputs.go @@ -28,6 +28,10 @@ type ImageInput struct { Service string // compose service name; also the lock pin key Entry string // image lock entry the generator renders for it Vars []string // variables that feed the rendered reference, precedence order + // NotInitDefault marks an entry whose version nself init never writes into .env + // (hasura: init writes v2.44.0, the hasura-v14 entry). Setting a variable to that + // version is a deliberate choice, so it is an override and not a default. + NotInitDefault bool } var imageInputs = []ImageInput{ @@ -36,7 +40,12 @@ var imageInputs = []ImageInput{ // pgvector repository from the major version. {Service: "postgres", Entry: "postgres", Vars: []string{"POSTGRES_IMAGE", "POSTGRES_VERSION"}}, {Service: "postgres", Entry: "pgvector", Vars: []string{"POSTGRES_IMAGE", "POSTGRES_VERSION"}}, - {Service: "hasura", Entry: "hasura", Vars: []string{"HASURA_VERSION"}}, + // hasura: the current release (v2.51.0). init does not write it, so a project that + // sets HASURA_VERSION to it moves off a lock pin of v2.44.0 (P7-LIVE-23). + {Service: "hasura", Entry: "hasura", Vars: []string{"HASURA_VERSION"}, NotInitDefault: true}, + // hasura-v14: graphql-engine v2.44.0, what v1.4 runs and the version nself init + // writes into .env (core_services.go, P7-LIVE-23); the same variable picks the tag. + {Service: "hasura", Entry: "hasura-v14", Vars: []string{"HASURA_VERSION"}}, {Service: "auth", Entry: "auth", Vars: []string{"AUTH_VERSION"}}, // auth-v14: nhost/auth 0.36.0, what v1.4 runs and v1.5 until AUTH_ENCRYPTION_KEY is // set (core_services_auth_nginx.go, P7-LIVE-20); the same variable picks the tag. @@ -126,7 +135,7 @@ func (in ImageInput) Set(getenv func(string) string) string { // the service renders (postgres: the postgres entry and the pgvector entry). func isServiceDefault(service, val string) bool { for _, row := range imageInputs { - if row.Service != service { + if row.Service != service || row.NotInitDefault { continue } if r, ok := LockedRef(row.Entry); ok && r.IsDefaultVersion(val) { diff --git a/internal/compose/images.lock.json b/internal/compose/images.lock.json index cbe97f066..554295dda 100644 --- a/internal/compose/images.lock.json +++ b/internal/compose/images.lock.json @@ -144,6 +144,21 @@ "name": "hasura", "role": "core", "repository": "docker.io/hasura/graphql-engine", + "version": "v2.51.0", + "index_digest": "sha256:5d435a6756de53709bf48c15b822cabab38d57acd22e656cf633cc2df67df150", + "platforms": { + "linux/amd64": "sha256:9981434098fd81809f78706e0dfa212650f5cc53746e5454e92712762aa77464", + "linux/arm64": "sha256:3baef699d7b05bb5e4335acdcd6f94eea36bb215205ae4d1b36d4efece227a94" + }, + "legacy_ref": "hasura/graphql-engine:v2.51.0", + "license": "Apache-2.0", + "source": "https://github.com/hasura/graphql-engine", + "mirror": "docker.io/nself/hasura" + }, + { + "name": "hasura-v14", + "role": "core", + "repository": "docker.io/hasura/graphql-engine", "version": "v2.44.0", "index_digest": "sha256:4d34476840601fa0c372bce9b1ed15aac6ffc3f0a0db677e81062764c08e1dc5", "platforms": { @@ -153,7 +168,7 @@ "legacy_ref": "hasura/graphql-engine:v2.44.0", "license": "Apache-2.0", "source": "https://github.com/hasura/graphql-engine", - "mirror": "docker.io/nself/hasura" + "mirror": "docker.io/nself/hasura-v14" }, { "name": "lego", diff --git a/internal/compose/images.yaml b/internal/compose/images.yaml index f0ebd001b..45168f334 100644 --- a/internal/compose/images.yaml +++ b/internal/compose/images.yaml @@ -27,7 +27,10 @@ images: - {name: elasticsearch, role: optional, repository: docker.elastic.co/elasticsearch/elasticsearch, version: 8.11.3, legacy_ref: "docker.elastic.co/elasticsearch/elasticsearch:8.11.3", license: "Elastic-2.0 OR SSPL-1.0 OR AGPL-3.0", source: "https://github.com/elastic/elasticsearch", platforms: [linux/amd64, linux/arm64]} - {name: functions, role: optional, repository: docker.io/nhost/functions, version: latest, legacy_ref: "nhost/functions:latest", license: MIT, source: "https://github.com/nhost/functions", platforms: [linux/amd64, linux/arm64]} - {name: grafana, role: monitoring, repository: docker.io/grafana/grafana, version: 11.1.0, legacy_ref: "grafana/grafana:11.1.0", license: AGPL-3.0, source: "https://github.com/grafana/grafana", platforms: [linux/amd64, linux/arm64]} - - {name: hasura, role: core, repository: docker.io/hasura/graphql-engine, version: v2.44.0, legacy_ref: "hasura/graphql-engine:v2.44.0", license: Apache-2.0, source: "https://github.com/hasura/graphql-engine", platforms: [linux/amd64, linux/arm64]} + # hasura: v1.4 renders hasura-v14 (v2.44.0), v1.5 renders hasura (v2.51.0); explicit HASURA_VERSION wins (EPIC D17, P7-LIVE-23). + # The v2.51 catalog migrations are one-way: rolling back means restoring the pre-update backup, then pinning back. + - {name: hasura, role: core, repository: docker.io/hasura/graphql-engine, version: v2.51.0, legacy_ref: "hasura/graphql-engine:v2.51.0", license: Apache-2.0, source: "https://github.com/hasura/graphql-engine", platforms: [linux/amd64, linux/arm64]} + - {name: hasura-v14, role: core, repository: docker.io/hasura/graphql-engine, version: v2.44.0, legacy_ref: "hasura/graphql-engine:v2.44.0", license: Apache-2.0, source: "https://github.com/hasura/graphql-engine", platforms: [linux/amd64, linux/arm64]} - {name: lego, role: tool, repository: docker.io/goacme/lego, version: v4.35.2, legacy_ref: "goacme/lego:v4.35.2", license: MIT, source: "https://github.com/go-acme/lego", platforms: [linux/amd64, linux/arm64]} - {name: loki, role: monitoring, repository: docker.io/grafana/loki, version: 3.1.0, legacy_ref: "grafana/loki:3.1.0", license: AGPL-3.0, source: "https://github.com/grafana/loki", platforms: [linux/amd64, linux/arm64]} - {name: mailpit, role: optional, repository: docker.io/axllent/mailpit, version: latest, legacy_ref: "axllent/mailpit:latest", license: MIT, source: "https://github.com/axllent/mailpit", platforms: [linux/amd64, linux/arm64]} diff --git a/internal/compose/images_lock_test.go b/internal/compose/images_lock_test.go index 1111ee3e8..ddeb35a34 100644 --- a/internal/compose/images_lock_test.go +++ b/internal/compose/images_lock_test.go @@ -113,6 +113,9 @@ func TestConfigDefaultVersionsStayPinned(t *testing.T) { if name == "auth" { // auth renders the entry its mode picks (P7-LIVE-20) name, _ = AuthEntry(nil) } + if name == "hasura" { // the config default is the v1.4 release (P7-LIVE-23) + name = "hasura-v14" + } r, ok := LockedRef(name) if !ok { continue @@ -123,7 +126,7 @@ func TestConfigDefaultVersionsStayPinned(t *testing.T) { } for name, cfg := range fixtureConfigs(t) { authEntry, _ := AuthEntry(cfg.Passthrough) - for svc, v := range map[string]string{"hasura": cfg.Hasura.Version, authEntry: cfg.Auth.Version, + for svc, v := range map[string]string{"hasura-v14": cfg.Hasura.Version, authEntry: cfg.Auth.Version, "elasticsearch": cfg.Search.Elasticsearch.Version} { if r, _ := LockedRef(svc); ImageRef(svc, v) != r.String() { t.Errorf("%s: ApplyDefaults %s version %q is unpinned in lock mode", name, svc, v) diff --git a/internal/compose/service_catalog.go b/internal/compose/service_catalog.go index e237cf3cd..eb2258353 100644 --- a/internal/compose/service_catalog.go +++ b/internal/compose/service_catalog.go @@ -187,6 +187,9 @@ func ServiceCatalog() []CatalogEntry { if key == "auth" { // the entry the mode renders without a key (P7-LIVE-20) key, _ = AuthEntry(nil) } + if key == "hasura" { // the entry the mode renders (P7-LIVE-23) + key = HasuraEntry() + } out[i].DefaultImage = DefaultImage(key) } return out diff --git a/internal/reconcile/testdata/fixtures/dev-minimal/golden-tree.txt b/internal/reconcile/testdata/fixtures/dev-minimal/golden-tree.txt index 52c30ca89..bffc7590a 100644 --- a/internal/reconcile/testdata/fixtures/dev-minimal/golden-tree.txt +++ b/internal/reconcile/testdata/fixtures/dev-minimal/golden-tree.txt @@ -15,7 +15,7 @@ d 0755 ssl/certificates f 0600 .env 2b4db21db0f5f82d03afa03af77b37a14407742b3632ec4910dd70a070a709a1 f 0600 .env.computed b6f45aa109d82fcb343e8483c1100906615feaab2a5c0309e6128a5eb8fcef4c f 0600 .nself/compose.env 9bcdc71d3104d8e0dcc25e1b1cc27d8f595b11c9ea2b1db33d7bc3fcd1f40364 -f 0600 docker-compose.yml 4ffd77d9c98e831ab580c23b3f6b91e21f8b7fb4b70d7b027830e49bf006d5d8 +f 0600 docker-compose.yml e2093d8e06e9a22514ea61700093196dc03da0ecdea0edfef3daa4123b359565 f 0644 .nself/build-profile a172cedcae47474b615c54d510a5d84a8dea3032e958587430b413538be3f333 f 0644 .nself/build-version 7fd890fffe22aa34fcf6b8f991d8abfbfed0090fe7cd76af60b46c20ad7bb027 f 0644 .nself/compose-env-files.txt 7d3e777d4d376c34edad2c78f512e08ec2d1819efdea2a89bbf91cb62e675a92 @@ -30,7 +30,7 @@ f 0644 nginx/nginx.conf 03dec7ab67a3584fce3014b7b5550f51ad9e070cd78e5a8f0c4b0da8 f 0644 nginx/sites/api-docs.conf e9c123c507404b40c062e15369038e9af46eb17841fa7135035675e702b6a4fb f 0644 nginx/sites/auth.conf 5e44d49fd8d1b8a6c998aaae1028cc834e5c9d12deedd3a471b0086cd7e6d666 f 0644 nginx/sites/hasura.conf 1f4dcdb4a34770e4ff0ccb4b463e81ed700e7736de90008502a20da70e55fe83 -f 0644 nself.lock.yaml 25c6972e66ed3dbddd4f937f91a0e1b21352fd122ba6d9bff298c02d8561bc4a +f 0644 nself.lock.yaml 6da3e0685fb2a1fe8ff815601b41d05e2192b5f95851e5bbfb918808431d4880 f 0644 postgres/init/01-init.sql 0238a6519fd10168af604249e45bf5234d3e6cec6ff9b927410a74ad90127637 f 0644 postgres/init/04-np-plugins.sql 6d2f7e0d1a984fe96b69df552bfd7ec37ae7ceef46b757aa078f8c44809b53ab f 0644 postgres/init/05-np-plugins-seed.sql 30949e128d86d27eb1db142ea98273de6d7251fae143b6ecdc9ef28ae0eb318a diff --git a/scripts/ci/hasura-upgrade-smoke.sh b/scripts/ci/hasura-upgrade-smoke.sh new file mode 100644 index 000000000..223c4ecf8 --- /dev/null +++ b/scripts/ci/hasura-upgrade-smoke.sh @@ -0,0 +1,570 @@ +#!/usr/bin/env bash +# Purpose: prove the P7-LIVE-23 Hasura move (graphql-engine v2.44.0 -> v2.51.0) on a real +# stack and print one PASS or FAIL line per assertion. +# NSELF_V15=1 (upgrade): +# 1. a new v1.5 project is pinned to the hasura-v14 ref (v2.44.0) in +# nself.lock.yaml (the pin edit stands in for `nself update`, P7-LIVE-10) +# and starts on it +# 2. a fixture migration (tables and rows) is applied with `nself db migrate`; +# metadata (tracked tables, a relationship, a select permission, an event +# trigger to a webhook receiver, a remote schema to a GraphQL receiver) is +# applied with `nself db hasura metadata apply` and exported with +# `nself db hasura metadata export` +# 3. the functional checks pass on v2.44.0: rows, permission, mutation, +# event trigger delivery, remote schema call, subscription event +# 4. a verified backup (nself backup create, verify, verify --restore-test) +# 5. the switch to the `hasura` ref (v2.51.0): the same functional checks, the +# metadata export equals the pre-switch export (files and normalised JSON), +# `nself db hasura diff` is clean, apply then export round-trips +# 6. rollback, unsupported path: pin back to v2.44.0 WITHOUT restoring the backup; +# the observed outcome is asserted (a catalog refusal, or a clean start that +# passes the functional checks, with the catalog versions in the message) +# 7. rollback, supported path: restore the pre-switch backup (after +# nself db reset --yes when the plain restore fails), keep the v2.44.0 pin, +# start cleanly; rows and metadata are the pre-switch ones, functional checks pass +# NSELF_V15 unset (baseline): steps 1 to 3 on v2.44.0 plus the metadata +# apply/export round trip; no switch. +# Usage: NSELF_V15=1 bash scripts/ci/hasura-upgrade-smoke.sh [path-to-nself-binary] +# env -u NSELF_V15 bash scripts/ci/hasura-upgrade-smoke.sh [path-to-nself-binary] +# Inputs: optional binary (built with go into the temp dir when absent); docker with the +# compose plugin and a reachable engine, curl (7.55+), python3 and openssl on PATH (nself +# build generates the dev certificate with it; go only when no binary is given). Image refs are read from internal/compose/images.lock.json +# (hasura, hasura-v14, python), never hardcoded. The stack publishes 127.0.0.1:8080 +# (Hasura), :5432, :80 and :443, so those must be free. TMPDIR must be a path the +# Docker engine can bind-mount. The webhook and GraphQL receivers are one synthetic +# container (scripts/ci/testdata/hasura-smoke/receiver.py) on the project network. +# Outputs: one line per assertion, "PASS " or "FAIL " (detail on stderr); +# the last line is "HASURA-SMOKE: PASS" or "HASURA-SMOKE: FAIL". Exit 1 on any +# FAIL, when nothing passed, or when a tool is missing. There is no SKIP: an +# assertion that cannot run is a FAIL. Admin secrets are never printed. +# Constraints: bash 3.2 compatible. The project, HOME and every secret file live in one +# 0700 temp dir; the EXIT trap removes the project's containers, volumes and +# networks (matched by the unique compose project label or exact container name) +# and then the dir. Only synthetic rows and runtime-generated secrets are used; +# the admin secret travels through 0600 files, never argv. No real project, +# server or remote is touched. +set -uo pipefail + +cd "$(dirname "$0")/../.." || exit 1 +ROOT="$PWD" +REAL_HOME="$HOME" +FIX="$ROOT/scripts/ci/testdata/hasura-smoke" + +PASSED=0 +FAILED=0 +pass() { printf 'PASS %s\n' "$1"; PASSED=$((PASSED + 1)); } +# redact: strip anything that looks like a JWT or a long hex secret from detail lines. +redact() { sed -E 's/eyJ[A-Za-z0-9_.-]+//g; s/[A-Fa-f0-9]{32,}//g'; } +fail() { + printf 'FAIL %s\n' "$1" + FAILED=$((FAILED + 1)) + if [ -n "${2:-}" ]; then printf ' %s\n' "$2" | redact >&2; fi +} +finish() { + printf '%s assertions: %s passed, %s failed\n' "$((PASSED + FAILED))" "$PASSED" "$FAILED" >&2 + if [ "$FAILED" -eq 0 ] && [ "$PASSED" -gt 0 ]; then + echo "HASURA-SMOKE: PASS" + exit 0 + fi + echo "HASURA-SMOKE: FAIL" + exit 1 +} +fatal() { fail "$@"; finish; } +info() { printf 'INFO %s\n' "$1"; } +step() { printf '== %s\n' "$1"; } +# check : PASS when the command exits 0. +check() { + local name="$1" + shift + if "$@" >/dev/null 2>&1; then pass "$name"; else fail "$name"; fi +} + +UPGRADE=0 +V15ENV="" +if [ "${NSELF_V15:-}" = "1" ]; then UPGRADE=1; V15ENV="NSELF_V15=1"; fi + +for t in docker curl python3; do + command -v "$t" >/dev/null 2>&1 || fatal "tool-$t-missing" +done +docker info >/dev/null 2>&1 || fatal "docker-engine-unreachable" +docker compose version >/dev/null 2>&1 || fatal "docker-compose-plugin-missing" + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/hasura-smoke.XXXXXX")" || fatal "mktemp-failed" +PROJECT="hasurasmoke$(python3 -c 'import secrets;print(secrets.token_hex(3))')" +PDIR="$WORK/project" +# shellcheck disable=SC2329 # invoked by the EXIT trap +cleanup() { + local ids + ids="$(docker ps -aq --filter "label=com.docker.compose.project=$PROJECT" 2>/dev/null)" + # backup verify --restore-test starts ${PROJECT}_pg_restore_test without a compose + # label, and the receiver is this script's own container: remove exactly those names. + ids="$ids $(docker ps -aq --filter "name=^${PROJECT}_pg_restore_test\$" 2>/dev/null)" + ids="$ids $(docker ps -aq --filter "name=^${PROJECT}_receiver\$" 2>/dev/null)" + # shellcheck disable=SC2086 + if [ -n "${ids// /}" ]; then docker rm -f -v $ids >/dev/null 2>&1; fi + ids="$(docker volume ls -q --filter "label=com.docker.compose.project=$PROJECT" 2>/dev/null)" + # shellcheck disable=SC2086 + if [ -n "$ids" ]; then docker volume rm -f $ids >/dev/null 2>&1; fi + ids="$(docker network ls -q --filter "label=com.docker.compose.project=$PROJECT" 2>/dev/null)" + # shellcheck disable=SC2086 + if [ -n "$ids" ]; then docker network rm $ids >/dev/null 2>&1; fi + rm -rf "$WORK" +} +trap cleanup EXIT +mkdir -p "$PDIR" "$WORK/home" +chmod 0700 "$WORK" +# Files that can hold secrets are created 0600 here and only ever truncated afterwards. +# (nself itself runs under umask 022: its build plan assumes 0644 files, D-0664.) +(umask 077; : > "$WORK/resp"; : > "$WORK/out"; : > "$WORK/err"; : > "$WORK/curl.err"; : > "$WORK/body"; : > "$WORK/admin.hdr"; : > "$WORK/admin.secret") + +# ---- the binary and the image refs --------------------------------------------------- +BIN="${1:-}" +if [ -z "$BIN" ]; then + command -v go >/dev/null 2>&1 || fatal "tool-go-missing (pass a built nself binary as the first argument)" + mkdir -p "$WORK/bin" + BIN="$WORK/bin/nself" + if CGO_ENABLED=0 go build -mod=vendor -buildvcs=false -o "$BIN" ./cmd/nself 2>"$WORK/build.err"; then + pass "build the nself binary" + else + redact < "$WORK/build.err" >&2 + fatal "build the nself binary" + fi +fi +case "$BIN" in /*) ;; *) BIN="$ROOT/$BIN" ;; esac +[ -x "$BIN" ] || fatal "binary $BIN is not executable" + +# lock_ref : repository:version@index_digest of an image lock entry. +lock_ref() { + python3 -c 'import json,sys +for e in json.load(open(sys.argv[1]))["images"]: + if e["name"] == sys.argv[2]: + print("%s:%s@%s" % (e["repository"], e["version"], e["index_digest"]))' "$ROOT/internal/compose/images.lock.json" "$1" +} +lock_field() { + python3 -c 'import json,sys +for e in json.load(open(sys.argv[1]))["images"]: + if e["name"] == sys.argv[2]: + print(e[sys.argv[3]])' "$ROOT/internal/compose/images.lock.json" "$1" "$2" +} +REF_V14="$(lock_ref hasura-v14)" +REF_NEW="$(lock_ref hasura)" +REF_PY="$(lock_ref python)" +LEGACY_V14="$(lock_field hasura-v14 legacy_ref)" +VER_V14="$(lock_field hasura-v14 version)" +VER_NEW="$(lock_field hasura version)" +if [ -n "$REF_V14" ] && [ -n "$REF_NEW" ] && [ "$REF_V14" != "$REF_NEW" ] && [ -n "$REF_PY" ]; then + pass "image lock has distinct hasura-v14 and hasura refs" +else + fatal "image lock has distinct hasura-v14 and hasura refs" +fi +info "host=$(uname -sm) engine=$(docker version --format '{{.Server.Os}}/{{.Server.Arch}}' 2>/dev/null) binary=$(uname -sm)" +info "hasura-v14=${REF_V14%%@*} hasura=${REF_NEW%%@*} mode=$([ "$UPGRADE" = 1 ] && echo upgrade || echo baseline)" + +# ---- helpers ------------------------------------------------------------------------- +HASURA_URL="http://127.0.0.1:8080" +RC=0 +# nsf : run nself in the project; stdout $WORK/out, stderr $WORK/err, status $RC. +nsf() { + local dh="" + RC=0 + if [ -n "${DOCKER_HOST:-}" ]; then dh="DOCKER_HOST=$DOCKER_HOST"; fi + # The caller's environment never steers nself at another stack: env -i plus a fixed + # allow-list (a db reset aimed at someone's own project would be destructive). + # shellcheck disable=SC2086 + (umask 022; cd "$PDIR" && env -i PATH="$PATH" HOME="$WORK/home" TMPDIR="${TMPDIR:-/tmp}" \ + DOCKER_CONFIG="${DOCKER_CONFIG:-$REAL_HOME/.docker}" $dh $V15ENV NSELF_CMD_LOG_ENABLED=false \ + AI_AUTO_INSTALL=false LANG="${LANG:-C}" "$BIN" "$@" > "$WORK/out" 2> "$WORK/err" < /dev/null) || RC=$? +} +nsf_tail() { { tail -n 6 "$WORK/err"; tail -n 4 "$WORK/out"; } | redact | tr '\n' '|'; } + +HTTP=000 +# jbody : write the next POST body to a 0600 file. +# shellcheck disable=SC2059 # the caller supplies the format +jbody() { printf "$@" > "$WORK/body"; } +# call [extra-header]: admin credentials from the 0600 header file; the POST +# body is $WORK/body; sets HTTP and writes the response to $WORK/resp. +call() { + local m="$1" u="$2" x="${3:-}" + : > "$WORK/resp" + set -- -sS -m 30 -o "$WORK/resp" -w '%{http_code}' -X "$m" -H "@$WORK/admin.hdr" + if [ -n "$x" ]; then set -- "$@" -H "$x"; fi + if [ "$m" = GET ]; then + HTTP="$(curl "$@" "$u" 2>"$WORK/curl.err" < /dev/null)" || HTTP=000 + else + HTTP="$(curl "$@" -H 'Content-Type: application/json' --data-binary "@$WORK/body" "$u" 2>"$WORK/curl.err")" || HTTP=000 + fi +} +# jv : evaluate against the last response; empty on any error. +jv() { + python3 -c 'import json,sys +try: + d = json.load(open(sys.argv[1])) + v = eval(sys.argv[2]) + print("" if v is None else v) +except Exception: + print("")' "$WORK/resp" "$1" +} +why() { + local m + m="$(jv 'd.get("error") or d.get("message") or d.get("errors")[0]["message"]' | cut -c1-140)" + printf 'http=%s %s' "$HTTP" "$m" +} +# gql [role]: POST a GraphQL query as admin, or as role with the same secret. +gql() { + jbody '{"query":%s}' "$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$1")" + if [ -n "${2:-}" ]; then call POST "$HASURA_URL/v1/graphql" "X-Hasura-Role: $2"; else call POST "$HASURA_URL/v1/graphql"; fi +} +dbq() { + printf '%s\n' "$1" | docker exec -i "${PROJECT}_postgres" psql -U postgres -d "${2:-$PROJECT}" -At -v ON_ERROR_STOP=1 -f - 2>/dev/null +} + +# catalog_version: the Hasura metadata catalog version stored in the database. +catalog_version() { dbq 'select version from hdb_catalog.hdb_version limit 1;'; } +compose_hasura_image() { + awk '/^ hasura:$/{f=1;next} f && /^ image:/{print $2;exit} /^ [a-z]/{f=0}' "$PDIR/docker-compose.yml" +} +lock_hasura_pin() { sed -n 's/^ hasura: //p' "$PDIR/nself.lock.yaml"; } +running_hasura_image() { docker inspect -f '{{.Config.Image}}' "${PROJECT}_hasura" 2>/dev/null; } +running_hasura_version() { call GET "$HASURA_URL/v1/version"; jv 'd["version"]'; } +# set_pin : stand-in for `nself update` (P7-LIVE-10, the official pin mover): rewrite +# the hasura pin line of nself.lock.yaml. +set_pin() { + sed "s|^ hasura: .*| hasura: $1|" "$PDIR/nself.lock.yaml" > "$PDIR/nself.lock.yaml.new" \ + && mv "$PDIR/nself.lock.yaml.new" "$PDIR/nself.lock.yaml" +} +# own_port: 127.0.0.1:8080 is published by this run's Hasura container, so no request can +# land in another stack. +own_port() { docker port "${PROJECT}_hasura" 2>/dev/null | grep -q '127.0.0.1:8080$'; } +# wait_hasura [seconds]: wait for this run's Hasura to answer /healthz. +wait_hasura() { + local i=0 max=$(( ${1:-240} / 3 )) + while [ "$i" -lt "$max" ]; do + if [ "$(curl -s -m 5 -o /dev/null -w '%{http_code}' "$HASURA_URL/healthz" 2>/dev/null)" = 200 ] && own_port; then + return 0 + fi + i=$((i + 1)) + sleep 3 + done + return 1 +} +# build_start