Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/wiki/Compat-V15.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
34 changes: 34 additions & 0 deletions .github/wiki/Image-Lock.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,40 @@ 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:<tag>`, 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 <id> --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 <id> --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) together with the database backup. On a project with a lock this is the route: the build that applied `HASURA_VERSION=v2.51.0` rewrote the lock pin, so removing the variable from the env alone leaves the lock and the compose file on v2.51.0. Only a project without `nself.lock.yaml` can go back by removing `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).

Caution: with no `nself.lock.yaml` and no `docker-compose.yml` (for example after `nself clean`), a v1.5 build has no pin to keep and starts Hasura on v2.51.0 with no notice. Take a verified backup before building an existing project with a v1.5 binary (tracked as debt).

## 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.
Expand Down
1 change: 1 addition & 0 deletions cmd/commands/start_lock_pins_render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var rowEnv = map[string]map[string]string{
"postgres/postgres": {"POSTGRES_EXTENSIONS": "uuid-ossp"},
"postgres/pgvector": {"POSTGRES_EXTENSIONS": "uuid-ossp,pgvector"},
"hasura/hasura": {},
"hasura/hasura-v14": {},
"auth/auth": {"NSELF_V15": "1", "MINIO_ENABLED": "false", "AUTH_ENCRYPTION_KEY": strings.Repeat("0f", 32)}, // fake key (P7-LIVE-20); v1.5 has no core MinIO (P7-PLUG-60)
"auth/auth-v14": {"AUTH_ENCRYPTION_KEY": strings.Repeat("0f", 32)}, // the matrix pins 1.2.3, a release that needs the key
"nginx/nginx": {},
Expand Down
3 changes: 3 additions & 0 deletions cmd/commands/start_lock_pins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ func newMatrixRow(t *testing.T, in compose.ImageInput) *matrixRow {
if ref, ok := compose.LockedRef(in.Entry); ok {
r.defVer, r.legacyVer = ref.Version, ref.LegacyVersion()
}
if in.NotInitDefault { // init never writes this entry's version, so setting it is an override (P7-LIVE-23)
r.defVer, r.legacyVer = "", ""
}
listed := map[string]bool{}
for _, v := range in.Vars {
listed[v] = true
Expand Down
78 changes: 78 additions & 0 deletions cmd/commands/update_plan_hasura.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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, treated as a one-way move), 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 (restore the backup to go back).

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())
v14, _ := compose.LockedRef("hasura-v14")
cur, _ := compose.LockedRef("hasura")
if !ok {
return plan.Candidate{Reason: plan.ReasonNoImage}
}
cmp, known := hasuraPinOrder(pin, r, v14, cur)
switch {
case !known:
return plan.Candidate{Reason: "the Hasura pin names a digest the CLI image lock does not know, so update cannot tell which release it is; it is left as is"}
case cmp > 0:
return plan.Candidate{Reason: fmt.Sprintf("the pinned Hasura is newer than the %s this mode offers; the pin-back is treated as one way, so 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 + " is treated as a one-way move: take a verified backup first, and restore it to go back (the metadata catalog version is the same on v2.44.0 and v2.51.0, but pinning back alone is unsupported)"}
}
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. known is false for a pin without a
// parseable tag whose digest is neither lock entry's (the lock cannot name the release).
// A digest-only pin of an entry orders as that entry's release.
func hasuraPinOrder(pin string, cand, v14, cur compose.Ref) (order int, known bool) {
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, true
}
}
for _, e := range []compose.Ref{v14, cur} {
if digest != "" && digest == e.IndexDigest {
return compareHasuraVersion(e.Version, cand.Version)
}
}
return 0, false
}

// 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
}
59 changes: 59 additions & 0 deletions cmd/commands/update_plan_hasura_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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"},
{"v1.5 digest-only v2.44.0 pin moves with a one-way note", true, "docker.io/hasura/graphql-engine@" + v14.IndexDigest, cur.String(), "one-way"},
{"v1.4 digest-only v2.44.0 pin stays", false, "docker.io/hasura/graphql-engine@" + v14.IndexDigest, v14.String(), ""},
{"v1.5 unknown digest-only pin is left as is", true, "docker.io/hasura/graphql-engine@sha256:" + strings.Repeat("cd", 32), "", "does not know"},
{"v1.4 unknown digest-only pin is left as is", false, "docker.io/hasura/graphql-engine@sha256:" + strings.Repeat("cd", 32), "", "does not know"},
} {
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)
}
})
}
}
4 changes: 4 additions & 0 deletions cmd/commands/update_plan_sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 4 additions & 2 deletions internal/build/lockfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -480,7 +480,9 @@ func TestLockBootstrapDockerIoSpelling(t *testing.T) {
for _, v15 := range []bool{false, true} {
f := newPlanFixture(t, "dev-minimal")
compattest.Set(t, v15)
r, ok := compose.LockedRef("hasura")
// The entry the mode renders: v1.4 hasura-v14 (the init-style default
// HASURA_VERSION=v2.44.0), v1.5 hasura (an explicit v2.51.0, an override).
r, ok := compose.LockedRef(compose.HasuraEntry())
if !ok {
t.Fatal("no hasura entry in the CLI image lock")
}
Expand Down
45 changes: 44 additions & 1 deletion internal/compose/core_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"sort"
"strings"

"github.com/nself-org/cli/internal/compat"
"github.com/nself-org/cli/internal/config"
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -198,3 +199,45 @@ 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 metadata catalog version is 48 on both v2.44.0 and v2.51.0 (P7-LIVE-23
// smoke), and v2.44.0 started cleanly on a database v2.51.0 had run on. Rollback
// is still supported only as restore-then-pin-back, by policy: the CLI does not
// check the catalog version (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: v2.44.0 in v1.4, v2.51.0 in v1.5 (in v1.5
// without a lock pin, v2.44.0 is the init default and renders hasura). The
// current release's own version (v2.51.0) renders hasura in any mode and, unlike
// the init default, is an override (image_inputs.go NotInitDefault). A project
// lock pin wins over the defaults, so a v1.5 project pinned to v2.44.0 stays on
// it until `nself update`. Any other value is an explicit tag, 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))
}
Loading
Loading