package hostclaim import ( "context" "encoding/json" "errors" "fmt" "os" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func plan15Enable(t *testing.T) { t.Helper() t.Setenv("CARLOS_HOST_CLAIM", "1") t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1") } func plan15Admit(o Observed, generation, region string) func(*Claim) error { h := o.Claim.Holder return AdmitWriter(Admission{ObservedOpID: o.Claim.Op.ID, ObservedPhase: o.Claim.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: generation, Region: region, PredecessorRegion: region, PredecessorCertified: true}) } func plan15Proof(t *testing.T, s release.Store, o Observed, generation, region string, want bool) { t.Helper() h := o.Claim.Holder got, err := AdmittedOrigin(t.Context(), s, o, h.Incarnation, h.Location, region, generation) if err != nil || got != want { t.Fatalf("proof %s/%s=%v,%v want %v", region, generation, got, err, want) } } func TestPlan15PruningMigrationAndScope(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "") t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "") s := release.NewDirStore(t.TempDir()) o := servingClaim(t, t.Context(), s) // A legacy claim can have more admissions than the covered retention window. for i := 0; i < 12; i++ { o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, fmt.Sprintf("old%d", i), "ie")) o = updateGenerationTest(t, t.Context(), s, o, func(c *Claim) error { for i := range c.Holder.Admissions { c.Holder.Admissions[i].Covered = true } return nil }) } if len(o.Claim.Holder.Admissions) != 8 { t.Fatal("fixture did not prune") } plan15Enable(t) // The chain starts empty: before the first recorded admission, even a // retained (and possibly injected) admission proves nothing. plan15Proof(t, s, o, "old0", "ie", false) plan15Proof(t, s, o, "old4", "ie", false) plan15Proof(t, s, o, "old11", "ie", false) o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "first", "ie")) for i := 0; i < 24; i++ { o = updateGenerationTest(t, t.Context(), s, o, func(c *Claim) error { for i := range c.Holder.Admissions { c.Holder.Admissions[i].Covered = true } return nil }) o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, fmt.Sprintf("new%d", i), "ie")) } for _, g := range []string{"first", "new0", "new23"} { plan15Proof(t, s, o, g, "ie", true) } plan15Proof(t, s, o, "old0", "ie", false) plan15Proof(t, s, o, "old4", "ie", false) plan15Proof(t, s, o, "old11", "ie", false) plan15Proof(t, s, o, "never", "ie", false) plan15Proof(t, s, o, "first", "uk", false) h := o.Claim.Holder for _, q := range []struct{ inc, loc string }{{strings.Repeat("b", 64), "instances/" + o.Host}, {h.Incarnation, "instances/" + o.Host}} { got, err := AdmittedOrigin(t.Context(), s, o, q.inc, q.loc, "ie", "first") if got || err != nil { t.Fatalf("wrong scope %v %v", got, err) } } // The old incarnation's history survives holder removal and the retirement cap. old := *h o = updateGenerationTest(t, t.Context(), s, o, func(c *Claim) error { c.Holder = nil c.Op = Op{} c.Obligations = nil c.RetiredIncarnations = nil return nil }) got, err := AdmittedOrigin(t.Context(), s, o, old.Incarnation, old.Location, "ie", "first") if !got || err != nil { t.Fatalf("retired origin %v %v", got, err) } } type plan15Store struct { release.Store failNode, failClaim, conflict, uncertain bool puts, gets int } func (s *plan15Store) Get(ctx context.Context, key string) ([]byte, string, error) { s.gets++ return s.Store.Get(ctx, key) } func (s *plan15Store) Put(ctx context.Context, key string, b []byte, tag string) (string, error) { s.puts++ if strings.HasPrefix(key, "control/host-origins/") && s.failNode { return "", os.ErrPermission } if strings.HasPrefix(key, "control/hosts/") { if s.failClaim { return "", os.ErrPermission } if s.conflict { return "", release.ErrCASConflict } if s.uncertain { _, err := s.Store.Put(ctx, key, b, tag) if err != nil { return "", err } return "", os.ErrDeadlineExceeded } } return s.Store.Put(ctx, key, b, tag) } func TestPlan15AtomicityAndRetry(t *testing.T) { for _, fault := range []string{"node", "claim", "conflict", "uncertain"} { t.Run(fault, func(t *testing.T) { plan15Enable(t) raw := release.NewDirStore(t.TempDir()) o := servingClaim(t, t.Context(), raw) s := &plan15Store{Store: raw, failNode: fault == "node", failClaim: fault == "claim", conflict: fault == "conflict", uncertain: fault == "uncertain"} if _, err := Update(t.Context(), s, o, plan15Admit(o, "candidate", "ie")); err == nil { t.Fatal("fault ignored") } fresh, err := Read(t.Context(), raw, o.Host) if err != nil { t.Fatal(err) } committed := fault == "uncertain" if (fresh.Claim.Holder.Generation == "candidate") != committed || (fresh.Claim.Origins != nil) != committed { t.Fatal("split commit") } plan15Proof(t, raw, fresh, "candidate", "ie", committed) if !committed { fresh = updateGenerationTest(t, t.Context(), raw, fresh, plan15Admit(fresh, "candidate", "ie")) plan15Proof(t, raw, fresh, "candidate", "ie", true) } if _, err := Update(t.Context(), raw, o, plan15Admit(o, "stale", "ie")); !errors.Is(err, ErrConflict) { t.Fatalf("stale: %v", err) } plan15Proof(t, raw, fresh, "stale", "ie", false) }) } } func TestPlan15NoSelfAuthentication(t *testing.T) { plan15Enable(t) raw := release.NewDirStore(t.TempDir()) o := servingClaim(t, t.Context(), raw) o = updateGenerationTest(t, t.Context(), raw, o, plan15Admit(o, "real", "ie")) h := o.Claim.Holder fake := originNode{V: 1, Host: o.Host, Count: o.Claim.Origins.Count + 1, Previous: o.Claim.Origins.Digest, Origin: originTuple{h.Incarnation, h.Location, "ie", "forged"}} body, _ := json.Marshal(fake) digest := originDigest(body) if _, err := raw.Put(t.Context(), originKey(o.Host, digest), body, ""); err != nil { t.Fatal(err) } plan15Proof(t, raw, o, "forged", "ie", false) // Even a caller who knows the ETag cannot substitute a new head in the reader. tampered := o clone := *o.Claim tampered.Claim = &clone clone.Origins = &OriginHead{digest, fake.Count} if ok, err := AdmittedOrigin(t.Context(), raw, tampered, h.Incarnation, h.Location, "ie", "forged"); ok || !errors.Is(err, ErrConflict) { t.Fatalf("edited observation: %v %v", ok, err) } for _, change := range []func(*Claim){func(c *Claim) { c.Origins = &OriginHead{digest, fake.Count} }, func(c *Claim) { c.Origins = nil }} { if _, err := Update(t.Context(), raw, o, func(c *Claim) error { change(c); return nil }); !errors.Is(err, ErrWrongState) { t.Fatalf("head bypass: %v", err) } } // Only the observed admissions change; the store and ETag are current. fresh, err := Read(t.Context(), raw, o.Host) if err != nil { t.Fatal(err) } fresh.Claim.Holder.Admissions = append(fresh.Claim.Holder.Admissions, AdmissionRecord{Region: "ie", Generation: "invented", Mode: "cold"}) if _, err := Update(t.Context(), raw, fresh, plan15Admit(fresh, "new", "ie")); !errors.Is(err, ErrConflict) { t.Fatalf("forged migration: %v", err) } // Missing and corrupt reachable nodes are errors, never an affirmative answer. key := originKey(o.Host, o.Claim.Origins.Digest) b, tag, err := raw.Get(t.Context(), key) if err != nil { t.Fatal(err) } if _, err = raw.Put(t.Context(), key, append(b, ' '), tag); err != nil { t.Fatal(err) } if ok, err := AdmittedOrigin(t.Context(), raw, o, h.Incarnation, h.Location, "ie", "real"); ok || err == nil { t.Fatal("corruption accepted") } if err = raw.Delete(t.Context(), key); err != nil { t.Fatal(err) } if ok, err := AdmittedOrigin(t.Context(), raw, o, h.Incarnation, h.Location, "ie", "real"); ok || err == nil { t.Fatal("missing node accepted") } } func TestPlan15FlagOffAndWarm(t *testing.T) { for _, flags := range [][2]string{{"", ""}, {"", "1"}, {"1", ""}} { t.Run(fmt.Sprint(flags), func(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", flags[0]) t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", flags[1]) raw := release.NewDirStore(t.TempDir()) o := servingClaim(t, t.Context(), raw) s := &plan15Store{Store: raw} var err error o, err = Update(t.Context(), s, o, plan15Admit(o, "legacy", "ie")) if err != nil { t.Fatal(err) } if o.Claim.Origins != nil || s.puts != 1 || s.gets != 0 { t.Fatal("flag-off wrote provenance") } b, _ := json.Marshal(o.Claim) if strings.Contains(string(b), "admitted_origins") { t.Fatal("flag-off wire changed") } }) } plan15Enable(t) raw := release.NewDirStore(t.TempDir()) o := servingClaim(t, t.Context(), raw) o = updateGenerationTest(t, t.Context(), raw, o, plan15Admit(o, "same", "ie")) before := *o.Claim.Origins o = updateGenerationTest(t, t.Context(), raw, o, AdmitWriter(Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "same", Box: o.Claim.Op.Exec, Mode: "warm", Region: "ie", PredecessorCertified: true})) if !reflect.DeepEqual(before, *o.Claim.Origins) { t.Fatal("warm duplicated origin") } plan15Proof(t, raw, o, "same", "ie", true) t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "") if _, err := Update(t.Context(), raw, o, plan15Admit(o, "gap", "ie")); !errors.Is(err, ErrWrongState) { t.Fatalf("disabled writer made gap: %v", err) } } func TestPlan15LimitsAndMalformedHistory(t *testing.T) { plan15Enable(t) s := release.NewDirStore(t.TempDir()) o := servingClaim(t, t.Context(), s) for _, gen := range []string{"../escape", RootGeneration, strings.Repeat("x", 257)} { if _, err := Update(t.Context(), s, o, plan15Admit(o, gen, "ie")); err == nil { t.Fatalf("bad generation %q", gen) } } o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "first", "ie")) h := o.Claim.Holder // A synthetic near-limit history tests refusal without writing a million nodes. node := originNode{V: 1, Host: o.Host, Count: originCountLimit, Previous: strings.Repeat("a", 64), Origin: originTuple{h.Incarnation, h.Location, "ie", "first"}} b, _ := json.Marshal(node) head := OriginHead{originDigest(b), originCountLimit} if _, err := s.Put(t.Context(), originKey(o.Host, head.Digest), b, ""); err != nil { t.Fatal(err) } o.Claim.Origins = &head key, _ := Key(o.Host) b, _ = json.Marshal(o.Claim) tag, err := s.Put(t.Context(), key, b, o.ETag) if err != nil { t.Fatal(err) } o.ETag = tag if _, err := Update(t.Context(), s, o, plan15Admit(o, "over-limit", "ie")); !errors.Is(err, ErrWrongState) { t.Fatalf("limit: %v", err) } ctx, cancel := context.WithCancel(t.Context()) cancel() if ok, err := AdmittedOrigin(ctx, s, o, h.Incarnation, h.Location, "ie", "absent"); ok || err == nil { t.Fatal("ignored cancellation") } node.Count-- b, _ = json.Marshal(node) // Same digest-shaped head with a mismatched count cannot prove membership. if _, err := s.Put(t.Context(), originKey(o.Host, originDigest(b)), b, ""); err != nil { t.Fatal(err) } o.Claim.Origins = &OriginHead{originDigest(b), originCountLimit} b, _ = json.Marshal(o.Claim) o.ETag, err = s.Put(t.Context(), key, b, o.ETag) if err != nil { t.Fatal(err) } if ok, err := AdmittedOrigin(t.Context(), s, o, h.Incarnation, h.Location, "ie", "first"); ok || err == nil { t.Fatal("count mismatch accepted") } for _, head := range []*OriginHead{{"bad", 1}, {strings.Repeat("a", 64), 0}, {strings.Repeat("a", 64), originCountLimit + 1}} { o.Claim.Origins = head if err := o.Claim.Validate(); err == nil { t.Fatal("bad head parsed") } } }