package release import ( "context" "reflect" "testing" "github.com/carlosframework/platform/internal/registry" ) // settings-5 task 8 (ruling R26): a hostname that moves to another tenant on // one box meets the old tenant's consumed file, keyed by hostname only. A // file tagged for another tenant is absent to root's decision and to the // next file root writes; an untagged file from before the tag is read as // today, so its completions and resolutions still count for the tenant that // has the route. // writeForeignConsumed is tenant A's record for host: a manual decision, a // resolved request, a failed completion watermark and a baking head. func writeForeignConsumed(t *testing.T, opt, host string) { t.Helper() a := Consumed{ Host: host, Account: "cdf", App: "km", Resolved: &ResolvedRequest{ID: 7, Target: "v0"}, Completed: 6, CompletedFailed: true, Desired: "v0", Reason: string(ReasonManual), Detail: "v0", Baking: "v9", Settings: SettingsDigest(UpdateSettings{Updates: UpdatesManual, Channel: "canary/x"}.Column()), } if err := WriteConsumed(opt, a); err != nil { t.Fatal(err) } } func TestAForeignConsumedFileIsAbsentForTheNewTenant(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) for _, ta := range [][2]string{{"cdf", "km"}, {"dee", "blog"}} { shipRelease(t, s, ta[0], ta[1], "v1", adoptPlatform, []byte("v1")) if err := Promote(ctx, s, priv, ta[0], ta[1], "canary/x", "v1"); err != nil { t.Fatal(err) } } t.Run("default settings: a fresh rule 4 decision with B's identity", func(t *testing.T) { opt, reg := t.TempDir(), testRegistry(t) const host = "a.example.com" if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", Account: "dee", App: "blog", Channel: "canary/x", Hibernate: true}); err != nil { t.Fatal(err) } writeForeignConsumed(t, opt, host) passWithSettings(t, reg, s, pub, opt) if v, _ := LiveVersion(opt, host); v != "v1" { t.Fatalf("live %q, want B's head v1 (today's adoption)", v) } c, err := ReadConsumed(opt, host) if err != nil || c == nil { t.Fatalf("consumed = %+v, %v", c, err) } want := Consumed{Schema: 1, Host: host, Account: "dee", App: "blog", Desired: "v1", Reason: string(ReasonAdopt), Detail: "v1", Settings: SettingsDigest(""), At: c.At} if !reflect.DeepEqual(*c, want) { t.Fatalf("B's consumed file:\n got %+v\nwant %+v (B's identity, none of A's bookkeeping)", *c, want) } }) t.Run("an install request: A's watermark never swallows B's id", func(t *testing.T) { opt, reg := t.TempDir(), testRegistry(t) const host = "b.example.com" if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", Account: "dee", App: "blog", Channel: "canary/x", Hibernate: true}); err != nil { t.Fatal(err) } if err := reg.SetUpdateSettings(host, UpdateSettings{Updates: UpdatesManual, Channel: "canary/x", InstallRequest: 3}.Column()); err != nil { t.Fatal(err) } if err := reg.SetBuildState(host, BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v0"}}.Column()); err != nil { t.Fatal(err) } writeForeignConsumed(t, opt, host) passWithSettings(t, reg, s, pub, opt) if v, _ := LiveVersion(opt, host); v != "v1" { t.Fatalf("live %q, want v1: B's request 3 resolved onto B's head", v) } c, _ := ReadConsumed(opt, host) if c == nil || c.Resolved == nil || *c.Resolved != (ResolvedRequest{ID: 3, Target: "v1"}) || c.Completed != 0 || c.CompletedFailed || c.Account != "dee" || c.App != "blog" { t.Fatalf("B's consumed file = %+v (resolved %+v)", c, c.Resolved) } }) } // An untagged file (root wrote it before ruling R22) is the route's own // record: read as today, it keeps a failed completion and a resolution that // only it holds, and root's next write tags it with the route's tenant. func TestAnUntaggedConsumedFileKeepsItsBookkeepingForTheSameTenant(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() priv, pub := testKeys(t) for _, v := range []string{"v1", "v2", "v3"} { shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v)) if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil { t.Fatal(err) } } manual5 := UpdateSettings{Updates: UpdatesManual, Channel: "canary/x", InstallRequest: 5} state := BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v1", Completed: 4}} t.Run("a failed completion only the file holds is not re-resolved", func(t *testing.T) { opt, reg := t.TempDir(), testRegistry(t) const host = "a.example.com" settingsRoute(t, reg, host, "km", "canary/x", manual5, state) legacy := Consumed{Host: host, Resolved: &ResolvedRequest{ID: 5, Target: "vbad"}, Completed: 5, CompletedFailed: true, Desired: "v1", Reason: string(ReasonManual), Detail: "v1"} if err := WriteConsumed(opt, legacy); err != nil { t.Fatal(err) } passWithSettings(t, reg, s, pub, opt) if v, _ := LiveVersion(opt, host); v != "v1" { t.Fatalf("live %q, want v1: request 5 is completed (failed), the instance is manual", v) } c, _ := ReadConsumed(opt, host) if c == nil || c.Completed != 5 || !c.CompletedFailed || c.Resolved == nil || *c.Resolved != (ResolvedRequest{ID: 5, Target: "vbad"}) { t.Fatalf("consumed = %+v (resolved %+v): the file's completion was lost", c, c.Resolved) } if c.Account != OpsAccount || c.App != "km" || c.Reason != string(ReasonManual) { t.Fatalf("consumed names %q/%q, reason %q", c.Account, c.App, c.Reason) } }) t.Run("a resolution only the file holds is followed", func(t *testing.T) { opt, reg := t.TempDir(), testRegistry(t) const host = "b.example.com" settingsRoute(t, reg, host, "km", "canary/x", manual5, state) legacy := Consumed{Host: host, Resolved: &ResolvedRequest{ID: 5, Target: "v2"}, Completed: 4, Desired: "v2", Reason: string(ReasonRequest), Detail: "v2"} if err := WriteConsumed(opt, legacy); err != nil { t.Fatal(err) } passWithSettings(t, reg, s, pub, opt) if v, _ := LiveVersion(opt, host); v != "v2" { t.Fatalf("live %q, want v2: request 5 was resolved to v2, not re-resolved onto the head", v) } c, _ := ReadConsumed(opt, host) if c == nil || c.Resolved == nil || *c.Resolved != (ResolvedRequest{ID: 5, Target: "v2"}) { t.Fatalf("consumed = %+v: the resolution was lost", c) } }) } // NextConsumed itself never copies another tenant's bookkeeping, whatever // its caller read: a tagged foreign file starts the next record empty, and // an untagged file is carried as today. func TestNextConsumedStartsEmptyOverAForeignFile(t *testing.T) { const host = "a.example.com" route := registry.Route{Host: host, Kind: "instance", Account: "dee", App: "blog"} known := BuildStateCopy{Known: true, Replica: true} dec := TargetDecision{Version: "v1", Reason: ReasonAdopt, Detail: "v1"} bookkeeping := Consumed{Host: host, Resolved: &ResolvedRequest{ID: 7, Target: "v0"}, Completed: 6, CompletedFailed: true, Baking: "v9"} foreign := bookkeeping foreign.Account, foreign.App = "cdf", "km" got := NextConsumed(TargetInput{Route: route, Build: known, Consumed: &foreign}, dec) want := Consumed{Host: host, Account: "dee", App: "blog", Desired: "v1", Reason: "adopt", Detail: "v1", Settings: SettingsDigest("")} if !reflect.DeepEqual(got, want) { t.Fatalf("over A's file:\n got %+v\nwant %+v", got, want) } sameAccount := bookkeeping sameAccount.Account, sameAccount.App = "dee", "km" if got := NextConsumed(TargetInput{Route: route, Build: known, Consumed: &sameAccount}, dec); got.Completed != 0 || got.Resolved != nil { t.Fatalf("another app on the same account passed its bookkeeping on: %+v", got) } legacy := bookkeeping got = NextConsumed(TargetInput{Route: route, Build: known, Consumed: &legacy}, dec) if got.Completed != 6 || !got.CompletedFailed || got.Resolved == nil || *got.Resolved != *legacy.Resolved { t.Fatalf("an untagged file lost its bookkeeping: %+v", got) } if got.Account != "dee" || got.App != "blog" { t.Fatalf("the untagged file was not tagged with the route's tenant: %q/%q", got.Account, got.App) } } // ForeignTo is the rule every reader shares: tagged, and for another tenant. func TestConsumedForeignTo(t *testing.T) { rt := registry.Route{Host: "a.example.com", Account: "dee", App: "blog"} cases := []struct { name string c *Consumed want bool }{ {"no file", nil, false}, {"untagged", &Consumed{}, false}, {"the route's tenant", &Consumed{Account: "dee", App: "blog"}, false}, {"another app", &Consumed{Account: "dee", App: "km"}, true}, {"another account", &Consumed{Account: "cdf", App: "blog"}, true}, {"half tagged, another account", &Consumed{Account: "cdf"}, true}, {"half tagged, the route's app", &Consumed{App: "blog"}, true}, } for _, c := range cases { if got := c.c.ForeignTo(rt); got != c.want { t.Errorf("%s: ForeignTo = %v, want %v", c.name, got, c.want) } } // A route with no valid account is the ops account's, as root tags it. ops := registry.Route{Host: "a.example.com", App: "km"} if (&Consumed{Account: OpsAccount, App: "km"}).ForeignTo(ops) { t.Error("the ops account's own file is foreign to its route") } } // The registry's Upsert compares tenants by registry.EffectiveAccount (Astra // r4 finding 2). It must be RouteAccount on the account, or the registry // keeps one tenant's settings columns for a route root reads as another's, // or drops them for one root reads as the same. func TestRegistryEffectiveAccountIsRouteAccount(t *testing.T) { for _, a := range []string{"", OpsAccount, "bes", "cdf", "ab", "ABC", "ab1", "abcdefghijklmnopq", "abcdefghijklmnop"} { if got, want := registry.EffectiveAccount(a), RouteAccount(registry.Route{Account: a}); got != want { t.Fatalf("account %q: registry.EffectiveAccount %q, RouteAccount %q", a, got, want) } } }