| index 049ad6f8e..9144dec5a 100644 |
| --- a/internal/host/updatesreport_test.go |
| +++ b/internal/host/updatesreport_test.go |
| @@ -11,7 +11,7 @@ import ( |
| |
| func TestTheReportCarriesRootsDecision(t *testing.T) { |
| b, _, reg := testBuildStates(t) |
| - if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Desired: "v1", Reason: "major", Detail: "v1", Completed: 2, Baking: "v4"}); err != nil { |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Account: release.OpsAccount, App: "km", Desired: "v1", Reason: "major", Detail: "v1", Completed: 2, Baking: "v4"}); err != nil { |
| t.Fatal(err) |
| } |
| two := 2 |
| @@ -37,8 +37,10 @@ func TestTheReportNamesTheTenant(t *testing.T) { |
| if err := reg.Upsert(registry.Route{Host: "b.example.com", Kind: "instance", Account: "bab", App: "shop", Hibernate: true, Socket: "/run/carlos/b.sock"}); err != nil { |
| t.Fatal(err) |
| } |
| + // Root names the tenant it decided for (R22); the report checks it. |
| + acct := map[string][2]string{"a.example.com": {release.OpsAccount, "km"}, "b.example.com": {"bab", "shop"}} |
| for _, h := range []string{"a.example.com", "b.example.com"} { |
| - if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: h, Desired: "v1", Reason: "manual"}); err != nil { |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: h, Account: acct[h][0], App: acct[h][1], Desired: "v1", Reason: "manual"}); err != nil { |
| t.Fatal(err) |
| } |
| } |
| @@ -86,7 +88,7 @@ func TestTheProtocolFollowsTheBuildStateService(t *testing.T) { |
| |
| func TestTheStatusProbesCarryTheProtocolAndTheDecisions(t *testing.T) { |
| b, _, reg := testBuildStates(t) |
| - if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Desired: "v1", Reason: "manual"}); err != nil { |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Account: release.OpsAccount, App: "km", Desired: "v1", Reason: "manual"}); err != nil { |
| t.Fatal(err) |
| } |
| var p status.Probes |
| @@ -100,7 +102,7 @@ func TestTheStatusProbesCarryTheProtocolAndTheDecisions(t *testing.T) { |
| // report must still be made, with the hold it carries (R-SETTINGS-UNREADABLE). |
| func TestTheReportSurvivesAnUnknownControlName(t *testing.T) { |
| b, _, reg := testBuildStates(t) |
| - if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Desired: "v1", Reason: "manual"}); err != nil { |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Account: release.OpsAccount, App: "km", Desired: "v1", Reason: "manual"}); err != nil { |
| t.Fatal(err) |
| } |
| col := `{"updates":"manual","channel":"stable","control":["updates","from-the-future"]}` |
| @@ -146,7 +148,7 @@ func TestAnUnknownReasonReportsNoRawDetail(t *testing.T) { |
| b, _, reg := testBuildStates(t) |
| raw := `update settings: invalid character 'x' looking for beginning of value` |
| for _, c := range []struct{ detail, want string }{{raw, ""}, {"v3", "v3"}} { |
| - if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Reason: "unknown", Detail: c.detail}); err != nil { |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Account: release.OpsAccount, App: "km", Reason: "unknown", Detail: c.detail}); err != nil { |
| t.Fatal(err) |
| } |
| got := updatesReport(reg, b.optDir)["a.example.com"] |
| @@ -154,7 +156,7 @@ func TestAnUnknownReasonReportsNoRawDetail(t *testing.T) { |
| t.Fatalf("detail %q: report = %+v, want detail %q", c.detail, got, c.want) |
| } |
| } |
| - if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Reason: "major", Detail: "not a version!"}); err != nil { |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Account: release.OpsAccount, App: "km", Reason: "major", Detail: "not a version!"}); err != nil { |
| t.Fatal(err) |
| } |
| if got := updatesReport(reg, b.optDir)["a.example.com"]; got.Detail != "not a version!" { |
| @@ -167,7 +169,7 @@ func TestAnUnknownReasonReportsNoRawDetail(t *testing.T) { |
| // alone, and a request is pending only above that. |
| func TestAnUnreadBuildStateReportsTheConsumedFileAlone(t *testing.T) { |
| b, _, reg := testBuildStates(t) |
| - if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Reason: "request", Completed: 2}); err != nil { |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: "a.example.com", Account: release.OpsAccount, App: "km", Reason: "request", Completed: 2}); err != nil { |
| t.Fatal(err) |
| } |
| set := func(req uint64) { |
| @@ -201,3 +203,159 @@ func TestAnUnreadBuildStateReportsTheConsumedFileAlone(t *testing.T) { |
| t.Fatalf("read: report = %+v; want completed 9, failed, nothing pending", got) |
| } |
| } |
| + |
| +// Astra r2 N1 (ruling R22): the consumed file is keyed by hostname only. A |
| +// hostname that moves from tenant A to tenant B on one box keeps A's file |
| +// until root decides for B; the report labels its entry with B's route, so |
| +// it must carry none of A's decision. B's own column (its major) stays. |
| +func TestAMovedHostnameReportsNoPreviousTenantsDecision(t *testing.T) { |
| + b, _, reg := testBuildStates(t) |
| + const host = "a.example.com" |
| + three, five := 3, 5 |
| + // Tenant A's route, and root's real write for it. |
| + a := registry.Route{Host: host, Kind: "instance", Account: "cdf", App: "shop", Hibernate: true, Socket: "/run/carlos/a.sock"} |
| + if err := reg.Upsert(a); err != nil { |
| + t.Fatal(err) |
| + } |
| + ua := release.UpdateSettings{Updates: release.UpdatesAuto, Channel: "stable", Major: &five, InstallRequest: 6} |
| + if err := reg.SetUpdateSettings(host, ua.Column()); err != nil { |
| + t.Fatal(err) |
| + } |
| + a.UpdateSettings = ua.Column() |
| + in := release.TargetInput{Route: a, Settings: ua, Pointer: &release.ChannelPointer{Channel: "stable", Version: "v-a-head"}, HeadHeld: "baking", |
| + Build: release.BuildStateCopy{Known: true, Replica: true, State: &release.BuildState{Schema: 1, Host: host}}} |
| + if err := release.WriteConsumed(b.optDir, release.NextConsumed(in, release.TargetDecision{Version: "v-a-pin", Reason: release.ReasonPin, Detail: "v-a-pin", Fail: 4})); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := updatesReport(reg, b.optDir)[host]; got.Desired != "v-a-pin" || got.Baking != "v-a-head" || got.Completed != 4 { |
| + t.Fatalf("A's own report = %+v; want A's decision (the fixture is wrong otherwise)", got) |
| + } |
| + // The route is retired (claims off: the row is deleted) and B's arrives on |
| + // the same hostname before root has decided for it. |
| + if err := reg.Delete(host); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", Account: "dee", App: "blog", Hibernate: true, Socket: "/run/carlos/a.sock"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + ub := release.UpdateSettings{Updates: release.UpdatesManual, Channel: "stable", Major: &three, InstallRequest: 2} |
| + if err := reg.SetUpdateSettings(host, ub.Column()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.SetBuildState(host, release.BuildStateCopy{Known: true, Replica: true, State: &release.BuildState{Schema: 1, Host: host, Completed: 1}}.Column()); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, ok := updatesReport(reg, b.optDir)[host] |
| + if !ok { |
| + t.Fatal("B's route has settings of its own and no entry") |
| + } |
| + if got.Account != "dee" || got.App != "blog" { |
| + t.Fatalf("B's entry names %s/%s, want dee/blog", got.Account, got.App) |
| + } |
| + if got.Desired != "" || got.Reason != "" || got.Detail != "" || got.Baking != "" || |
| + got.Completed != 0 || got.CompletedFailed || got.Pending != 0 { |
| + t.Fatalf("B's entry carries a decision root did not make for B: %+v", got) |
| + } |
| + if got.Major == nil || *got.Major != 3 { |
| + t.Fatalf("B's entry major = %v, want B's own 3", got.Major) |
| + } |
| + rep, err := updatesReader(reg, b.optDir)(host) |
| + if err != nil || rep.Desired != "" || rep.Reason != "" { |
| + t.Fatalf("B's socket report = %+v, %v; want none of A's decision", rep, err) |
| + } |
| + if rep.Settings.Updates != release.UpdatesManual || rep.Settings.Major == nil || *rep.Settings.Major != 3 { |
| + t.Fatalf("B's socket report settings = %+v, want B's own", rep.Settings) |
| + } |
| + |
| + // Root's next pass decides for B, over A's file: now the decision is B's. |
| + b2 := registry.Route{Host: host, Kind: "instance", Account: "dee", App: "blog", UpdateSettings: ub.Column()} |
| + prev, err := release.ReadConsumed(b.optDir, host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + inB := release.TargetInput{Route: b2, Settings: ub, Consumed: prev, |
| + Build: release.BuildStateCopy{Known: true, Replica: true, State: &release.BuildState{Schema: 1, Host: host, Completed: 1}}} |
| + if err := release.WriteConsumed(b.optDir, release.NextConsumed(inB, release.TargetDecision{Version: "v-b", Reason: release.ReasonManual, Detail: "v-b"})); err != nil { |
| + t.Fatal(err) |
| + } |
| + got = updatesReport(reg, b.optDir)[host] |
| + if got.Account != "dee" || got.App != "blog" || got.Desired != "v-b" || got.Reason != "manual" || got.Detail != "v-b" { |
| + t.Fatalf("B's entry after root decided for B = %+v; want B's decision", got) |
| + } |
| + if rep, err := updatesReader(reg, b.optDir)(host); err != nil || rep.Desired != "v-b" || rep.Reason != "manual" { |
| + t.Fatalf("B's socket report after root decided for B = %+v, %v", rep, err) |
| + } |
| +} |
| + |
| +// R22: a consumed file from before root named the tenant is decided for no |
| +// one: the entry names the route's tenant and carries the route's own major, |
| +// and nothing that came from the file or the build state. |
| +func TestAnUnattributedConsumedFileReportsNoDecision(t *testing.T) { |
| + b, _, reg := testBuildStates(t) |
| + const host = "a.example.com" |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: host, Desired: "v1", Reason: "pin", Detail: "v1", |
| + Baking: "v2", Completed: 3, CompletedFailed: true}); err != nil { |
| + t.Fatal(err) |
| + } |
| + two := 2 |
| + if err := reg.SetUpdateSettings(host, release.UpdateSettings{Updates: release.UpdatesAuto, Channel: "stable", Major: &two, InstallRequest: 7}.Column()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.SetBuildState(host, release.BuildStateCopy{Known: true, Replica: true, State: &release.BuildState{Schema: 1, Host: host, Completed: 5}}.Column()); err != nil { |
| + t.Fatal(err) |
| + } |
| + got := updatesReport(reg, b.optDir)[host] |
| + if got.Account != release.OpsAccount || got.App != "km" || got.Major == nil || *got.Major != 2 { |
| + t.Fatalf("entry = %+v; want tenant %s/km and the route's own major", got, release.OpsAccount) |
| + } |
| + if got.Desired != "" || got.Reason != "" || got.Detail != "" || got.Baking != "" || |
| + got.Completed != 0 || got.CompletedFailed || got.Pending != 0 { |
| + t.Fatalf("an unattributed file's decision was reported: %+v", got) |
| + } |
| + if rep, err := updatesReader(reg, b.optDir)(host); err != nil || rep.Desired != "" || rep.Reason != "" { |
| + t.Fatalf("socket report = %+v, %v; want no decision from an unattributed file", rep, err) |
| + } |
| + // A route with default settings and only an unattributed file has |
| + // nothing of its own to report, as before root decides for it. |
| + if err := reg.SetUpdateSettings(host, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := updatesReport(reg, b.optDir); got != nil { |
| + t.Fatalf("report = %+v; want none for a default route with no decision of its own", got) |
| + } |
| +} |
| + |
| +// R22: a file that names the route's tenant is reported in full; one that |
| +// names the same account and another app, or another account and the same |
| +// app, is not. |
| +func TestAConsumedFileIsReportedOnlyForTheTenantItNames(t *testing.T) { |
| + b, _, reg := testBuildStates(t) |
| + const host = "a.example.com" |
| + if err := reg.SetUpdateSettings(host, release.UpdateSettings{Updates: release.UpdatesAuto, Channel: "stable", InstallRequest: 7}.Column()); err != nil { |
| + t.Fatal(err) |
| + } |
| + write := func(account, app string) { |
| + t.Helper() |
| + if err := release.WriteConsumed(b.optDir, release.Consumed{Schema: 1, Host: host, Account: account, App: app, |
| + Desired: "v1", Reason: "pin", Detail: "v1", Baking: "v2", Completed: 3, CompletedFailed: true}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + write(release.OpsAccount, "km") |
| + got := updatesReport(reg, b.optDir)[host] |
| + if got.Desired != "v1" || got.Reason != "pin" || got.Detail != "v1" || got.Baking != "v2" || |
| + got.Completed != 3 || !got.CompletedFailed || got.Pending != 7 { |
| + t.Fatalf("matching file: entry = %+v; want the whole decision", got) |
| + } |
| + if rep, err := updatesReader(reg, b.optDir)(host); err != nil || rep.Desired != "v1" || rep.Reason != "pin" { |
| + t.Fatalf("matching file: socket report = %+v, %v", rep, err) |
| + } |
| + for _, other := range [][2]string{{release.OpsAccount, "shop"}, {"cdf", "km"}, {"", "km"}, {release.OpsAccount, ""}} { |
| + write(other[0], other[1]) |
| + got := updatesReport(reg, b.optDir)[host] |
| + if got.Desired != "" || got.Reason != "" || got.Detail != "" || got.Baking != "" || |
| + got.Completed != 0 || got.CompletedFailed || got.Pending != 0 { |
| + t.Fatalf("file for %v: entry = %+v; want no decision", other, got) |
| + } |
| + } |
| +} |