| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/activator" |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/provision" |
| 15 | "github.com/carlosframework/platform/internal/registry" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | func plan14ProofFixture(t *testing.T) (*proofRootEnv, registry.Route, provision.Record, *proofBindingSource) { |
| 20 | t.Helper() |
| 21 | e := newProofRootEnv(t, provePolicy) |
| 22 | rec := provision.Record{Schema: 1, Host: observedHost, Account: "bab", App: "console", Kind: "instance", |
| 23 | Unit: "console.service", Addr: "127.0.0.1:4000", Channel: "canary/console", Pin: "v1", Health: &provision.Health{Path: "/ready"}} |
| 24 | plan14PutRecord(t, e.store, rec) |
| 25 | health, err := json.Marshal(rec.Health.WithDefaults()) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | rt := registry.Route{Host: rec.Host, Account: rec.Account, App: rec.App, Kind: rec.Kind, Unit: rec.Unit, |
| 30 | Addr: rec.Addr, Health: string(health), Channel: rec.Channel, Status: "ready"} |
| 31 | if err := e.root.reg.Upsert(rt); err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | unitsDir := t.TempDir() |
| 35 | fragment := filepath.Join(unitsDir, rec.Unit) |
| 36 | if err := os.WriteFile(fragment, []byte("[Service]\nExecStart="+release.LiveLink(e.root.obs.optDir, rec.Host)+"\n"), 0644); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | f := e.root.obs.sys.(*fakeUnits) |
| 40 | f.shows[0].Name, f.shows[0].FragmentPath = rec.Unit, fragment |
| 41 | f.exe = map[int]string{7: release.VersionPath(e.root.obs.optDir, rec.Account, rec.Pin, rec.App)} |
| 42 | source := &proofBindingSource{store: e.store, sys: f, optDir: e.root.obs.optDir, openFile: os.Open, unitDirs: []string{unitsDir}} |
| 43 | e.root.obs.resolve = source.resolve |
| 44 | e.root.obs.ready = func(registry.Route) func() error { return func() error { return nil } } |
| 45 | return e, rt, rec, source |
| 46 | } |
| 47 | |
| 48 | func plan14PutRecord(t *testing.T, s release.Store, rec provision.Record) { |
| 49 | t.Helper() |
| 50 | key := provision.RecordKey(rec.Account, rec.App, rec.Host) |
| 51 | _, etag, err := s.Get(context.Background(), key) |
| 52 | if err != nil && !errors.Is(err, os.ErrNotExist) { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | body, err := json.Marshal(rec) |
| 56 | if err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | if _, err := s.Put(context.Background(), key, body, etag); err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | func TestPlan14ProofObservationRejectsForgedRegistry(t *testing.T) { |
| 65 | t.Parallel() |
| 66 | mutations := map[string]func(*registry.Route){ |
| 67 | "account": func(r *registry.Route) { r.Account = "bap" }, |
| 68 | "app": func(r *registry.Route) { r.App = "other" }, |
| 69 | "host": func(r *registry.Route) { r.Host = "other.example.com" }, |
| 70 | "unit": func(r *registry.Route) { r.Unit = "other.service" }, |
| 71 | "socket": func(r *registry.Route) { r.Socket = "/run/forged.sock" }, |
| 72 | "addr": func(r *registry.Route) { r.Addr = "" }, |
| 73 | "addrs": func(r *registry.Route) { r.Addrs = "127.0.0.1:1" }, |
| 74 | "health nil": func(r *registry.Route) { r.Health = "" }, |
| 75 | "health disabled": func(r *registry.Route) { r.Health = "null" }, |
| 76 | "health alternate": func(r *registry.Route) { r.Health = `{"path":"/forged"}` }, |
| 77 | "all upstreams empty": func(r *registry.Route) { r.Addr, r.Addrs, r.Socket, r.Health = "", "", "", "" }, |
| 78 | } |
| 79 | for name, mutate := range mutations { |
| 80 | t.Run(name, func(t *testing.T) { |
| 81 | e, rt, _, _ := plan14ProofFixture(t) |
| 82 | mutate(&rt) |
| 83 | if err := e.root.reg.Delete(observedHost); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | if err := e.root.reg.Upsert(rt); err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | if got := e.root.observeAndRecord(rt, 15*time.Minute); got.err == nil { |
| 90 | t.Fatal("forged route recorded as healthy") |
| 91 | } |
| 92 | e.root.run(context.Background()) |
| 93 | if err := e.root.obs.recordSettled(e.bake)(rt); err == nil { |
| 94 | t.Fatal("forged settled route accepted") |
| 95 | } |
| 96 | keys, err := e.store.List(context.Background(), "apps/") |
| 97 | if err != nil || len(keys) != 1 { |
| 98 | t.Fatalf("unexpected proof writes: %v, %v", keys, err) |
| 99 | } |
| 100 | for _, acct := range []string{"bab", "bap"} { |
| 101 | for _, app := range []string{"console", "other"} { |
| 102 | if got := release.ReadHealthy(e.bake, acct, app); len(got) != 0 { |
| 103 | t.Fatalf("forged health %s/%s: %+v", acct, app, got) |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | }) |
| 108 | } |
| 109 | t.Run("canonical failing readiness", func(t *testing.T) { |
| 110 | e, rt, _, _ := plan14ProofFixture(t) |
| 111 | e.root.obs.ready = func(r registry.Route) func() error { |
| 112 | if r.Addr == "" { |
| 113 | return nil |
| 114 | } |
| 115 | return func() error { return os.ErrPermission } |
| 116 | } |
| 117 | e.root.run(context.Background()) |
| 118 | if e.root.obs.recordSettled(e.bake)(rt) == nil || len(release.ReadHealthy(e.bake, "bab", "console")) != 0 || e.proof(t) != nil { |
| 119 | t.Fatal("failed canonical readiness authorized health") |
| 120 | } |
| 121 | }) |
| 122 | t.Run("canonical success", func(t *testing.T) { |
| 123 | e, rt, _, _ := plan14ProofFixture(t) |
| 124 | e.root.run(context.Background()) |
| 125 | if err := e.root.obs.recordSettled(e.bake)(rt); err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | if len(release.ReadHealthy(e.bake, "bab", "console")) != 1 || e.proof(t) == nil { |
| 129 | t.Fatal("canonical healthy build was not recorded and proved") |
| 130 | } |
| 131 | }) |
| 132 | } |
| 133 | |
| 134 | func TestPlan14ProofBindingChangesAfterObservation(t *testing.T) { |
| 135 | t.Parallel() |
| 136 | e, _, rec, _ := plan14ProofFixture(t) |
| 137 | obs := e.root.recordLive(context.Background()) |
| 138 | rec.Health.Path = "/changed" |
| 139 | plan14PutRecord(t, e.store, rec) |
| 140 | e.root.mintAndLog(context.Background(), obs) |
| 141 | if e.proof(t) != nil { |
| 142 | t.Fatal("stale binding minted a proof") |
| 143 | } |
| 144 | rt, err := e.root.reg.Get(rec.Host) |
| 145 | if err != nil { |
| 146 | t.Fatal(err) |
| 147 | } |
| 148 | health, err := json.Marshal(rec.Health.WithDefaults()) |
| 149 | if err != nil { |
| 150 | t.Fatal(err) |
| 151 | } |
| 152 | rt.Health = string(health) |
| 153 | if err := e.root.reg.Upsert(*rt); err != nil { |
| 154 | t.Fatal(err) |
| 155 | } |
| 156 | e.root.run(context.Background()) |
| 157 | if e.proof(t) == nil { |
| 158 | t.Fatal("new successful observation did not mint") |
| 159 | } |
| 160 | t.Run("etag only", func(t *testing.T) { |
| 161 | e, _, rec, _ := plan14ProofFixture(t) |
| 162 | obs := e.root.recordLive(context.Background()) |
| 163 | rec.RecordSeq++ |
| 164 | plan14PutRecord(t, e.store, rec) |
| 165 | e.root.obs.ready = func(registry.Route) func() error { return func() error { return os.ErrPermission } } |
| 166 | e.root.mintAndLog(context.Background(), obs) |
| 167 | if e.proof(t) != nil { |
| 168 | t.Fatal("changed ETag reused cached observation") |
| 169 | } |
| 170 | }) |
| 171 | } |
| 172 | |
| 173 | func TestPlan14OrdinaryInstanceBinding(t *testing.T) { |
| 174 | t.Parallel() |
| 175 | e, rt, rec, source := plan14ProofFixture(t) |
| 176 | rec.Addr, rec.Unit = "", "" |
| 177 | plan14PutRecord(t, e.store, rec) |
| 178 | rt.Addr, rt.Unit, rt.Socket, rt.Backing = "", "", provision.DefaultSocket(rec.Host), registry.BackingUnit |
| 179 | if err := e.root.reg.Upsert(rt); err != nil { |
| 180 | t.Fatal(err) |
| 181 | } |
| 182 | u := source.sys.(*fakeUnits) |
| 183 | u.shows[0].Name = activator.UnitName(rec.Host) |
| 184 | if got := e.root.observeAndRecord(rt, 0); got.err == nil { |
| 185 | t.Fatal("ordinary record recorded root health") |
| 186 | } |
| 187 | if got := release.ReadHealthy(e.bake, rt.Account, rt.App); len(got) != 0 { |
| 188 | t.Fatalf("ordinary health: %+v", got) |
| 189 | } |
| 190 | rt.Unit = "other.service" |
| 191 | if got := e.root.observeAndRecord(rt, 0); got.err == nil { |
| 192 | t.Fatal("forged ordinary unit accepted") |
| 193 | } |
| 194 | rec.Unit = "other.service" |
| 195 | plan14PutRecord(t, e.store, rec) |
| 196 | if _, err := source.forHost(context.Background(), rt.Host); err == nil { |
| 197 | t.Fatal("record overrides ordinary naming") |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | func TestPlan14RootRestartDispatchFromRecord(t *testing.T) { |
| 202 | t.Parallel() |
| 203 | e, rt, rec, source := plan14ProofFixture(t) |
| 204 | rec.Unit = "" |
| 205 | rt.Unit = "" |
| 206 | plan14PutRecord(t, e.store, rec) |
| 207 | needs, err := source.needsRestart(context.Background(), rt) |
| 208 | if err != nil || needs { |
| 209 | t.Fatalf("unmanaged proxy restart: %v %v", needs, err) |
| 210 | } |
| 211 | rec.Unit = "console.service" |
| 212 | plan14PutRecord(t, e.store, rec) |
| 213 | if _, err := source.needsRestart(context.Background(), rt); err == nil { |
| 214 | t.Fatal("cleared candidate unit suppressed authoritative restart") |
| 215 | } |
| 216 | rt.Unit = rec.Unit |
| 217 | needs, err = source.needsRestart(context.Background(), rt) |
| 218 | if err != nil || !needs { |
| 219 | t.Fatalf("named service skipped: %v %v", needs, err) |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | func TestPlan14ProofClaimBinding(t *testing.T) { |
| 224 | t.Parallel() |
| 225 | e, rt, rec, s := plan14ProofFixture(t) |
| 226 | observed := hostclaim.Observed{Host: rec.Host, ETag: "claim-one", Claim: &hostclaim.Claim{Host: rec.Host, Holder: &hostclaim.Holder{Key: provision.RecordKey(rec.Account, rec.App, rec.Host), Account: rec.Account, App: rec.App, Incarnation: "inc", Stamp: 1, Session: 1, WriterBox: "box"}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}}} |
| 227 | s.claim = func(context.Context, string) (hostclaim.Observed, error) { return observed, nil } |
| 228 | s.box = func(context.Context) (string, error) { return "box", nil } |
| 229 | cached := e.root.recordLive(context.Background()) |
| 230 | if cached.byHost[rt.Host].err != nil { |
| 231 | t.Fatal(cached.byHost[rt.Host].err) |
| 232 | } |
| 233 | observed.Claim.Holder.App = "other" |
| 234 | e.root.mintAndLog(context.Background(), cached) |
| 235 | if e.proof(t) != nil { |
| 236 | t.Fatal("changed holder minted proof") |
| 237 | } |
| 238 | if _, err := s.forHost(context.Background(), rt.Host); err == nil { |
| 239 | t.Fatal("cross-app holder accepted") |
| 240 | } |
| 241 | observed.Claim.Holder.App = rec.App |
| 242 | observed.Claim.Holder.WriterBox = "other-box" |
| 243 | if _, err := s.forHost(context.Background(), rt.Host); err == nil { |
| 244 | t.Fatal("foreign writer accepted") |
| 245 | } |
| 246 | } |
| 247 | |