package provision import ( "testing" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // updatesFixture is reconcile_test.go's own setup (an account, the "tito" // app's instances manifest, a DirStore, a temp registry) with the three // moves these tests make: write the app's manifest with a given update // policy, run a pass, read a row. type updatesFixture struct { t *testing.T s *release.DirStore sqid string reg *registry.Registry cfg ReconcileConfig } func newUpdatesFixture(t *testing.T) *updatesFixture { t.Helper() s, sqid := setup(t) return &updatesFixture{t: t, s: s, sqid: sqid, reg: regWith(t), cfg: ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}} } // putManifest replaces the app's instances manifest with one carrying p. func (fx *updatesFixture) putManifest(p *UpdatePolicy) { fx.t.Helper() m := Manifest{Schema: 1, App: "tito", Domain: "go.tito.io", Pool: "tito", Seed: []string{"seed"}, Updates: p} putReplace(fx.t, fx.s, ManifestKey(fx.sqid, "tito"), mustJSON(fx.t, m)) } // corruptManifest leaves unparseable bytes at the manifest's key: a read // failure that is not absence. func (fx *updatesFixture) corruptManifest() { fx.t.Helper() putReplace(fx.t, fx.s, ManifestKey(fx.sqid, "tito"), []byte("{")) } func (fx *updatesFixture) putRecord(rec Record) { fx.t.Helper() if err := CreateRecord(ctx, fx.s, rec); err != nil { fx.t.Fatal(err) } } func (fx *updatesFixture) run() { fx.t.Helper() if err := Reconcile(ctx, fx.s, fx.reg, fx.cfg); err != nil { fx.t.Fatal(err) } } func (fx *updatesFixture) row(host string) registry.Route { return *mustGet(fx.t, fx.reg, host) } func (fx *updatesFixture) hasRow(host string) bool { fx.t.Helper() rt, err := fx.reg.Get(host) if err != nil { fx.t.Fatal(err) } return rt != nil } func TestReconcileCarriesSettingsToTheRow(t *testing.T) { fx := newUpdatesFixture(t) fx.putManifest(&UpdatePolicy{DefaultUpdates: release.UpdatesManual}) fx.putRecord(acmeRecord(fx.sqid)) fx.run() rt := fx.row("acme.go.tito.io") u, err := release.ParseUpdateSettings(rt.UpdateSettings) if err != nil || u.Updates != release.UpdatesManual { t.Fatalf("minted row settings %q: %+v %v", rt.UpdateSettings, u, err) } // The default changes: the row follows on the next pass. fx.putManifest(nil) fx.run() if rt = fx.row("acme.go.tito.io"); rt.UpdateSettings != "" { t.Fatalf("after the policy was removed: %q", rt.UpdateSettings) } } func TestAnUntouchedFleetConvergesNothing(t *testing.T) { fx := newUpdatesFixture(t) fx.putRecord(acmeRecord(fx.sqid)) fx.run() if fx.row("acme.go.tito.io").UpdateSettings != "" { t.Fatal("a record with no settings wrote the column") } fx.run() if fx.row("acme.go.tito.io").UpdateSettings != "" { t.Fatal("a second pass wrote the column") } } // Review Focus 2. func TestUnreadablePolicyLeavesTheColumnAlone(t *testing.T) { fx := newUpdatesFixture(t) fx.putManifest(&UpdatePolicy{DefaultUpdates: release.UpdatesManual}) fx.putRecord(acmeRecord(fx.sqid)) fx.run() fx.corruptManifest() fx.run() if u, _ := release.ParseUpdateSettings(fx.row("acme.go.tito.io").UpdateSettings); u.Updates != release.UpdatesManual { t.Fatal("an unreadable policy reset the instance to auto") } } // Astra I1 #7: an instance that inherits manual from its app, minted (on a // fresh box, or after a move) while the app's manifest cannot be read, must // not be minted as auto: it would adopt the head instead of recovering // last_executed. The mint waits until the policy reads; a genuinely absent // manifest is known (no policy) and mints at once. func TestMintWaitsForAReadablePolicy(t *testing.T) { fx := newUpdatesFixture(t) fx.putManifest(&UpdatePolicy{DefaultUpdates: release.UpdatesManual}) fx.corruptManifest() fx.putRecord(acmeRecord(fx.sqid)) fx.run() if fx.hasRow("acme.go.tito.io") { t.Fatal("minted while the app's update policy was unreadable") } fx.putManifest(&UpdatePolicy{DefaultUpdates: release.UpdatesManual}) fx.run() if u, _ := release.ParseUpdateSettings(fx.row("acme.go.tito.io").UpdateSettings); u.Updates != release.UpdatesManual { t.Fatalf("minted with %q once the policy read", fx.row("acme.go.tito.io").UpdateSettings) } } // Ruling R5: the moves held mint is a mint like any other. It waits while the // policy is unreadable, then lands held with the inherited settings. func TestHeldMintWaitsForAReadablePolicy(t *testing.T) { fx := newUpdatesFixture(t) fx.cfg.Region = "uk" fx.cfg.Hold = func(Record) (string, string, bool) { return "landing", "mv_acme", true } fx.putManifest(&UpdatePolicy{DefaultUpdates: release.UpdatesManual}) fx.corruptManifest() fx.putRecord(movingRecord(fx.sqid)) fx.run() if fx.hasRow("acme.go.tito.io") { t.Fatal("held mint landed while the app's update policy was unreadable") } fx.putManifest(&UpdatePolicy{DefaultUpdates: release.UpdatesManual}) fx.run() rt := fx.row("acme.go.tito.io") if rt.Status != "landing" || rt.HoldOp != "mv_acme" { t.Fatalf("row is %q held by %q, want landing by mv_acme", rt.Status, rt.HoldOp) } if u, _ := release.ParseUpdateSettings(rt.UpdateSettings); u.Updates != release.UpdatesManual { t.Fatalf("held mint carries %q once the policy read", rt.UpdateSettings) } }