package console import ( "context" "encoding/json" "fmt" "net/http" "strings" "testing" "time" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/status" ) func updatesBox(label string, enforce bool, hosts map[string]status.HostUpdates, routes map[string]string) status.Box { m := &status.BoxMetrics{Updates: hosts, RouteVersions: routes} if enforce { m.UpdateSettings = status.UpdateSettingsProtocol } return status.Box{Label: label, GeneratedAt: time.Now(), Metrics: m} } func TestUpdatesJoinCarriesTheReason(t *testing.T) { two := 2 j := joinBoxes([]status.Box{updatesBox("ie-1", true, map[string]status.HostUpdates{"a.shop.example.com": {Desired: "v1", Reason: "major", Detail: "v2", Baking: "v4", Major: &two, Pending: 7, Completed: 6, CompletedFailed: true, Account: "bab", App: "shop"}}, map[string]string{"a.shop.example.com": "v1"})}, time.Now()) u := j.update("a.shop.example.com", "bab", "shop") if u == nil || !u.Enforced || u.Desired != "v1" || u.Reason != "major" || u.Detail != "v2" || u.Baking != "v4" || u.Major == nil || *u.Major != 2 || u.Pending != 7 || u.Completed != 6 || !u.CompletedFailed { t.Fatalf("join = %+v", u) } } // enforcedOnly says u is the join's answer for a report it cannot attribute: // whether the box enforces settings, and nothing else. func enforcedOnly(u *updatesJoinView, enforced bool) bool { return u != nil && *u == updatesJoinView{Enforced: enforced} } // Astra r1 (R18): a report that names no tenant, on a box with no rollout // report for the host, may be another tenant's: the hostname may have been // retired and given to this one while the snapshot is still fresh. A // platform box's report is no exception. The join answers only whether the // box enforces settings, so an old box still reads "not enforced". func TestATenantlessReportJoinsOnlyWhetherTheBoxEnforces(t *testing.T) { now := time.Now() secret := map[string]status.HostUpdates{"h": {Desired: "v-a-desired", Reason: "pin", Detail: "v-a-pin", Baking: "v-a-baking", Pending: 4, Completed: 3, CompletedFailed: true}} for _, enforce := range []bool{true, false} { if u := joinBoxes([]status.Box{updatesBox("platform", enforce, secret, map[string]string{"h": "v1"})}, now).update("h", "bab", "shop"); !enforcedOnly(u, enforce) { t.Fatalf("platform box (enforces %v): tenantless report joined as %+v", enforce, u) } var j boxJoin j.joinFleetRollouts([]status.Box{updatesBox("fleet", enforce, secret, map[string]string{"h": "v1"})}, now) if u := j.update("h", "bab", "shop"); !enforcedOnly(u, enforce) { t.Fatalf("fleet box (enforces %v): tenantless report joined as %+v", enforce, u) } } } // R18: an entry that names its own tenant needs no rollout report, and its // own word wins over the rollout report's. func TestAnAttributedReportNeedsNoRolloutReport(t *testing.T) { now := time.Now() b := updatesBox("platform", true, map[string]status.HostUpdates{"h": {Reason: "manual", Detail: "v1", Account: "bab", App: "shop"}}, map[string]string{"h": "v1"}) j := joinBoxes([]status.Box{b}, now) if u := j.update("h", "bab", "shop"); u == nil || u.Reason != "manual" || u.Detail != "v1" || !u.Enforced { t.Fatalf("the named tenant = %+v", u) } for _, other := range [][2]string{{"cdf", "shop"}, {"bab", "blog"}} { if u := j.update("h", other[0], other[1]); u != nil { t.Fatalf("%s/%s joined bab/shop's report: %+v", other[0], other[1], u) } } b.Metrics.Rollout = &status.RolloutReport{Hosts: map[string]status.HostRollout{"h": {Account: "cdf", App: "blog", Installed: "v1"}}} j = joinBoxes([]status.Box{b}, now) if u := j.update("h", "cdf", "blog"); u != nil { t.Fatalf("the rollout report's tenant outranked the entry's own: %+v", u) } if u := j.update("h", "bab", "shop"); u == nil || u.Reason != "manual" { t.Fatalf("the entry's own tenant = %+v", u) } } // Review Focus 3: a box's report about a host never surfaces on another // tenant's list. The box's own rollout report names the host's tenant. func TestUpdatesJoinRefusesAnotherTenantsReport(t *testing.T) { b := updatesBox("ie-1", true, map[string]status.HostUpdates{"a.shop.example.com": {Reason: "manual"}}, nil) b.Metrics.Rollout = &status.RolloutReport{Hosts: map[string]status.HostRollout{ "a.shop.example.com": {Account: "cdf", App: "blog", Installed: "v1"}}} j := joinBoxes([]status.Box{b}, time.Now()) if u := j.update("a.shop.example.com", "bab", "shop"); u != nil { t.Fatalf("another tenant's report was joined: %+v", u) } if u := j.update("a.shop.example.com", "bab", "blog"); u != nil { t.Fatalf("another account's report was joined on a matching app: %+v", u) } if u := j.update("a.shop.example.com", "cdf", "blog"); u == nil || u.Reason != "manual" { t.Fatalf("the named tenant lost its own report: %+v", u) } } func TestAnOldBoxIsNotEnforced(t *testing.T) { j := joinBoxes([]status.Box{updatesBox("ie-1", false, nil, map[string]string{"a.shop.example.com": "v1"})}, time.Now()) if u := j.update("a.shop.example.com", "bab", "shop"); u == nil || u.Enforced { t.Fatalf("an old box's host = %+v; want a view saying not enforced", u) } } // A fleet box's report names no tenant unless its rollout report does, and a // customer's box is not trusted to speak for a host on its own say-so: all // that joins is whether the box enforces settings (R18). func TestAFleetBoxReportWithoutATenantIsRefused(t *testing.T) { now := time.Now() fleet := updatesBox("fleet", true, map[string]status.HostUpdates{"h": {Reason: "manual"}}, map[string]string{"h": "v1"}) var j boxJoin j.joinFleetRollouts([]status.Box{fleet}, now) if u := j.update("h", "bab", "shop"); !enforcedOnly(u, true) { t.Fatalf("a tenantless fleet report was joined: %+v", u) } // With the tenant from its own rollout report it is this tenant's, and // only this tenant's. fleet.Metrics.Rollout = &status.RolloutReport{Hosts: map[string]status.HostRollout{"h": {Account: "bab", App: "shop", Installed: "v1"}}} j = boxJoin{} j.joinFleetRollouts([]status.Box{fleet}, now) if u := j.update("h", "bab", "shop"); u == nil || u.Reason != "manual" { t.Fatalf("a fleet report naming this tenant = %+v", u) } if u := j.update("h", "cdf", "shop"); u != nil { t.Fatalf("a fleet report naming bab was joined for cdf: %+v", u) } } // A platform box's word wins over a fleet box's for the same host, as the // rollout join's does. func TestAFleetBoxNeverOverridesAPlatformBoxUpdates(t *testing.T) { now := time.Now() view := func(label, reason string) status.Box { b := updatesBox(label, true, map[string]status.HostUpdates{"h": {Reason: reason}}, map[string]string{"h": "v1"}) b.Metrics.Rollout = &status.RolloutReport{Hosts: map[string]status.HostRollout{"h": {Account: "bab", App: "shop", Installed: "v1"}}} return b } j := joinBoxes([]status.Box{view("platform", "manual")}, now) j.joinFleetRollouts([]status.Box{view("fleet", "pin")}, now) if u := j.update("h", "bab", "shop"); u == nil || u.Reason != "manual" { t.Fatalf("h = %+v, want the platform box's report", u) } var empty boxJoin empty.joinFleetRollouts([]status.Box{view("fleet", "pin")}, now) if u := empty.update("h", "bab", "shop"); u == nil || u.Reason != "pin" { t.Fatalf("h on an empty join = %+v, want the fleet box's report", u) } } func TestAStaleBoxReportsNothing(t *testing.T) { b := updatesBox("ie-1", true, map[string]status.HostUpdates{"h": {Reason: "manual"}}, map[string]string{"h": "v1"}) b.GeneratedAt = time.Now().Add(-24 * time.Hour) if u := joinBoxes([]status.Box{b}, time.Now()).update("h", "bab", "shop"); u != nil { t.Fatalf("a stale box's report was joined: %+v", u) } } // The list door: update_report rides beside the record's own updates key, // the other tenant's host carries none, and settings_default tells an // untouched record from one that asked for a hold. func TestAPIInstanceListCarriesTheUpdateReport(t *testing.T) { fx, token := instanceFixture(t, "code-inst-updates") enableInstances(t, fx, "titogo", titoManifest()) u := instancesURL(fx, "bab", "titogo") for _, h := range []string{"acme.go.tito.io", "beta.go.tito.io"} { if code, _, out := rawReq(t, http.MethodPost, u, token, fmt.Sprintf(`{"host":%q,"region":"ie"}`, h)); code != http.StatusCreated { t.Fatalf("create %s = %d %s", h, code, out) } } box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now()) box.Metrics = &status.BoxMetrics{ UpdateSettings: status.UpdateSettingsProtocol, RouteVersions: map[string]string{"acme.go.tito.io": "v1", "beta.go.tito.io": "v9"}, Updates: map[string]status.HostUpdates{ "acme.go.tito.io": {Desired: "v1", Reason: "manual", Detail: "v1"}, "beta.go.tito.io": {Reason: "pin", Detail: "v9"}, }, Rollout: &status.RolloutReport{Hosts: map[string]status.HostRollout{ "acme.go.tito.io": {Account: "bab", App: "titogo", Installed: "v1"}, // A report naming another app under this host: never joined. "beta.go.tito.io": {Account: "bab", App: "other", Installed: "v9"}, }}, } if err := status.WriteBox(context.Background(), fx.rel, box); err != nil { t.Fatal(err) } type listed struct { Host string `json:"host"` Updates string `json:"updates"` SettingsDefault bool `json:"settings_default"` UpdateReport *updatesJoinView `json:"update_report"` } list := func() []listed { code, _, out := rawReq(t, http.MethodGet, u, token, "") if code != http.StatusOK { t.Fatalf("list = %d %s", code, out) } var l struct { Instances []listed `json:"instances"` } if err := json.Unmarshal([]byte(out), &l); err != nil { t.Fatal(err) } return l.Instances } for _, r := range list() { switch r.Host { case "acme.go.tito.io": if r.UpdateReport == nil || r.UpdateReport.Reason != "manual" || !r.UpdateReport.Enforced { t.Errorf("acme update_report = %+v", r.UpdateReport) } case "beta.go.tito.io": if r.UpdateReport != nil { t.Errorf("beta joined another app's report: %+v", r.UpdateReport) } if !r.SettingsDefault || r.Updates != "" { t.Errorf("beta never asked for settings: default=%v updates=%q", r.SettingsDefault, r.Updates) } } } } // The app's policy reaches settings_default: a manual default makes every // record a non-default one without the record saying so itself. func TestAPIInstanceListSettingsDefaultFollowsThePolicy(t *testing.T) { fx, token := instanceFixture(t, "code-inst-updates-default") m := titoManifest() m.Updates = &provision.UpdatePolicy{DefaultUpdates: "manual"} enableInstances(t, fx, "titogo", m) u := instancesURL(fx, "bab", "titogo") if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"acme.go.tito.io","region":"ie"}`); code != http.StatusCreated { t.Fatalf("create = %d %s", code, out) } code, _, out := rawReq(t, http.MethodGet, u, token, "") if code != http.StatusOK { t.Fatalf("list = %d %s", code, out) } var l struct { Instances []struct { SettingsDefault bool `json:"settings_default"` } `json:"instances"` } if err := json.Unmarshal([]byte(out), &l); err != nil || len(l.Instances) != 1 { t.Fatalf("decode %v: %s", err, out) } if l.Instances[0].SettingsDefault { t.Fatalf("a manual app default read as default: %s", out) } } // Astra r1 (R18), through the list door: box1 (a platform box with no // rollout report, as with CARLOS_SWAP off) still reports the update state // it last had for acme under its previous tenant. The hostname now belongs // to bab/titogo. The list carries whether the box enforces settings and // nothing of the report, and the CLI's REASON cell is empty. func TestAReassignedHostNeverListsThePreviousTenantsReport(t *testing.T) { fx, token := instanceFixture(t, "code-inst-updates-reassigned") enableInstances(t, fx, "titogo", titoManifest()) u := instancesURL(fx, "bab", "titogo") if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"acme.go.tito.io","region":"ie"}`); code != http.StatusCreated { t.Fatalf("create = %d %s", code, out) } box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now()) box.Metrics = &status.BoxMetrics{ UpdateSettings: status.UpdateSettingsProtocol, RouteVersions: map[string]string{"acme.go.tito.io": "v1"}, Updates: map[string]status.HostUpdates{ "acme.go.tito.io": {Desired: "v-prev-desired", Reason: "pin", Detail: "v-prev-pin", Baking: "v-prev-baking", Pending: 9, Completed: 8}, }, } if err := status.WriteBox(context.Background(), fx.rel, box); err != nil { t.Fatal(err) } code, _, out := rawReq(t, http.MethodGet, u, token, "") if code != http.StatusOK { t.Fatalf("list = %d %s", code, out) } for _, leak := range []string{"v-prev-desired", "v-prev-pin", "v-prev-baking", `"reason":"pin"`, `"pending"`, `"completed"`} { if strings.Contains(out, leak) { t.Errorf("the list leaked %s: %s", leak, out) } } var l struct { Instances []struct { Host string `json:"host"` UpdateReport *updatesJoinView `json:"update_report"` } `json:"instances"` } if err := json.Unmarshal([]byte(out), &l); err != nil || len(l.Instances) != 1 { t.Fatalf("decode %v: %s", err, out) } if r := l.Instances[0].UpdateReport; !enforcedOnly(r, true) { t.Fatalf("acme update_report = %+v, want enforced and nothing else", r) } }