fix/c7-moves-intent
Squashed from fix/c7-moves-intent into main.
2 files changed,
+60
−19
internal/hostclaim/relocating.go+15 −10internal/hostclaim/relocating_test.go+45 −9
CI passed — run details
diff --git a/internal/hostclaim/relocating.go b/internal/hostclaim/relocating.go| index b5565460c..b3f27d7b5 100644 |
| --- a/internal/hostclaim/relocating.go |
| +++ b/internal/hostclaim/relocating.go |
| @@ -66,7 +66,10 @@ func ConvertMovingToRelocating(ctx context.Context, s release.Store, observed Ob |
| if opID == "" || before.Op.ID != opID { |
| return Observed{}, ErrStaleOp |
| } |
| - if before.Holder == nil || before.Op.Intent["admission_id"] == "" || before.Holder.Family != "instance" || before.Op.Kind != KindMoving || before.Op.Phase == "landing" || before.Op.Phase == "committed" { |
| + // The move's own intent, as BeginMove writes it: "source" and "target", |
| + // plus the source enrollment the request door records from the roster. |
| + source := before.Op.Intent["source"] |
| + if before.Holder == nil || !validBoxReference(source) || source == "none" || before.Op.Intent["source_enrollment"] == "" || before.Holder.Family != "instance" || before.Op.Kind != KindMoving || before.Op.Phase == "landing" || before.Op.Phase == "committed" { |
| return Observed{}, ErrWrongState |
| } |
| // Check the instance record even while the claim still says moving: the |
| @@ -79,7 +82,7 @@ func ConvertMovingToRelocating(ctx context.Context, s release.Store, observed Ob |
| if err != nil { |
| return Observed{}, err |
| } |
| - if region == "" || region != fence.binding.Region || before.Op.Intent["source_box"] != fence.record.Box || before.Op.Intent["source_enrollment"] != ref.Enrollment || before.Op.Intent["source_region"] != region || (before.Op.Exec != fence.record.Box && before.Op.Exec != "none" && before.Op.Exec != "") { |
| + if region == "" || region != fence.binding.Region || source != fence.record.Box || before.Op.Intent["source_enrollment"] != ref.Enrollment || (before.Op.Exec != fence.record.Box && before.Op.Exec != "none" && before.Op.Exec != "") { |
| return Observed{}, ErrFence |
| } |
| raw, err := json.Marshal(ref) |
| @@ -112,7 +115,9 @@ func ConvertMovingToRelocating(ctx context.Context, s release.Store, observed Ob |
| c.Op.Journal["source_generation"] = c.Holder.Generation |
| c.Op.Journal["roster_revision"] = strconv.FormatUint(fence.binding.RosterRevision, 10) |
| c.Op.Journal["roster_etag"] = fence.binding.RosterETag |
| - c.Op.Journal["admission_id"] = before.Op.Intent["admission_id"] |
| + if admission := before.Op.Intent["admission_id"]; admission != "" { |
| + c.Op.Journal["admission_id"] = admission |
| + } |
| c.Op.Journal["conversion_claim_gen"] = strconv.FormatUint(before.Gen, 10) |
| c.Op.Journal["conversion_claim_etag"] = observed.ETag |
| c.Op.Kind, c.Op.Phase, c.Op.Exec = KindRelocating, "pinning", "none" |
| @@ -120,16 +125,16 @@ func ConvertMovingToRelocating(ctx context.Context, s release.Store, observed Ob |
| c.Op.Intent = map[string]string{} |
| } |
| c.Op.Intent["region"] = region |
| + c.Op.Intent["source_box"] = source |
| // The original move target is in a different region. It is not a selected |
| // same-region replacement. Keep the move history without authorizing it. |
| - if target := c.Op.Intent["target_box"]; target != "" { |
| - c.Op.Intent["move_target_box"] = target |
| - delete(c.Op.Intent, "target_box") |
| - } |
| - if enrollment := c.Op.Intent["target_enrollment"]; enrollment != "" { |
| - c.Op.Intent["move_target_enrollment"] = enrollment |
| - delete(c.Op.Intent, "target_enrollment") |
| + for from, to := range map[string]string{"target": "move_target_box", "to": "move_to_region", "target_enrollment": "move_target_enrollment"} { |
| + if v := c.Op.Intent[from]; v != "" { |
| + c.Op.Intent[to] = v |
| + } |
| + delete(c.Op.Intent, from) |
| } |
| + delete(c.Op.Intent, "source") |
| participants := make([]string, 0, len(c.Op.Participants)) |
| for _, box := range c.Op.Participants { |
| if box != fence.record.Box { |
diff --git a/internal/hostclaim/relocating_test.go b/internal/hostclaim/relocating_test.go| index 88d2cf2a7..d7a13ab81 100644 |
| --- a/internal/hostclaim/relocating_test.go |
| +++ b/internal/hostclaim/relocating_test.go |
| @@ -34,6 +34,7 @@ func (a c7Authority) ValidateFence(_ context.Context, f termfence.Record) (Fence |
| type c7Guard struct { |
| calls int |
| rejectAt int |
| + region string |
| } |
| func (g *c7Guard) CheckMovingBeforeFlip(_ context.Context, o Observed, id string, ref FenceRef) (string, error) { |
| @@ -44,6 +45,9 @@ func (g *c7Guard) CheckMovingBeforeFlip(_ context.Context, o Observed, id string |
| if g.rejectAt > 0 && g.calls >= g.rejectAt { |
| return "", ErrWrongState |
| } |
| + if g.region != "" { |
| + return g.region, nil |
| + } |
| return "ie", nil |
| } |
| @@ -65,7 +69,8 @@ func c7Fixture(t *testing.T) (*c7Store, Observed, FenceRef, c7Authority) { |
| o, err = Update(ctx, s, o, func(c *Claim) error { |
| c.Holder.Generation, c.Holder.Stamp = "gen-current", 17 |
| c.Op = Op{ID: "move-1", Kind: KindMoving, Phase: "releasing", Exec: "adi-knitting", |
| - Intent: map[string]string{"admission_id": "admission-existing", "source_box": "adi-knitting", "source_enrollment": strings.Repeat("a", 32), "source_region": "ie", "target_box": "bri-knitting", "target_enrollment": strings.Repeat("c", 32)}, |
| + // The intent BeginMove writes, plus the source enrollment the door adds. |
| + Intent: map[string]string{"to": "uk", "source": "adi-knitting", "target": "bri-knitting", "source_enrollment": strings.Repeat("a", 32)}, |
| Journal: map[string]string{"pin": "keep"}, Participants: []string{"adi-knitting", "bri-knitting"}} |
| c.Obligations = []Obligation{{ID: "staging", Op: "move-1", Kind: "park-replica", Region: "uk", Owner: "region:uk", Retirement: "retire-1", Park: &ParkScope{Location: "moves/" + testHost + "/move-1", Incarnation: c.Holder.Incarnation, EvidenceAt: time.Now().UTC(), RequestLimit: 120 * time.Second}}} |
| return nil |
| @@ -95,7 +100,7 @@ func TestC7ConversionPreservesOwnershipAndChores(t *testing.T) { |
| t.Fatal(err) |
| } |
| c := after.Claim |
| - if c.Op.Journal["admission_id"] != "admission-existing" || c.Op.Journal["source_stamp"] != "17" || c.Op.Journal["source_generation"] != "gen-current" || c.Op.Journal["roster_revision"] != "7" || c.Op.Journal["roster_etag"] != "roster-etag" || c.Op.Journal["reason"] != "source-terminated-before-flip" || c.Op.Journal["conversion_claim_etag"] != before.ETag { |
| + if _, ok := c.Op.Journal["admission_id"]; ok || c.Op.Journal["source_stamp"] != "17" || c.Op.Journal["source_generation"] != "gen-current" || c.Op.Journal["roster_revision"] != "7" || c.Op.Journal["roster_etag"] != "roster-etag" || c.Op.Journal["reason"] != "source-terminated-before-flip" || c.Op.Journal["conversion_claim_etag"] != before.ETag { |
| t.Fatalf("missing conversion authority: %+v", c.Op.Journal) |
| } |
| if s.puts != 1 || c.Gen != before.Claim.Gen+1 || guard.calls != 2 { |
| @@ -107,7 +112,7 @@ func TestC7ConversionPreservesOwnershipAndChores(t *testing.T) { |
| if c.Op.ID != "move-1" || c.Op.Kind != KindRelocating || c.Op.Phase != "pinning" || c.Op.Exec != "none" || c.Op.Intent["region"] != "ie" { |
| t.Fatalf("wrong conversion: %+v", c.Op) |
| } |
| - if c.Op.Intent["target_box"] != "" || c.Op.Intent["move_target_box"] != "bri-knitting" || !reflect.DeepEqual(c.Op.Participants, []string{"bri-knitting"}) { |
| + if c.Op.Intent["target_box"] != "" || c.Op.Intent["target"] != "" || c.Op.Intent["move_target_box"] != "bri-knitting" || c.Op.Intent["move_to_region"] != "uk" || c.Op.Intent["source_box"] != "adi-knitting" || !reflect.DeepEqual(c.Op.Participants, []string{"bri-knitting"}) { |
| t.Fatalf("target or barrier: %+v", c.Op) |
| } |
| var saved FenceRef |
| @@ -123,7 +128,7 @@ func TestC7ConversionPreservesOwnershipAndChores(t *testing.T) { |
| } |
| func TestC7ConversionRefusesWithoutWriting(t *testing.T) { |
| - for _, tc := range []string{"landing-record", "flip-during-verify", "landing-claim", "wrong-op", "wrong-source", "wrong-enrollment", "wrong-region", "foreign-exec", "journal-rebind", "authority", "digest", "no-guard", "missing-admission"} { |
| + for _, tc := range []string{"landing-record", "flip-during-verify", "landing-claim", "wrong-op", "wrong-source", "wrong-enrollment", "wrong-region", "foreign-exec", "journal-rebind", "authority", "digest", "no-guard", "missing-enrollment", "missing-source"} { |
| t.Run(tc, func(t *testing.T) { |
| s, o, ref, a := c7Fixture(t) |
| id := "move-1" |
| @@ -138,11 +143,11 @@ func TestC7ConversionRefusesWithoutWriting(t *testing.T) { |
| case "wrong-op": |
| id = "other" |
| case "wrong-source": |
| - o.Claim.Op.Intent["source_box"] = "other" |
| + o.Claim.Op.Intent["source"] = "other" |
| case "wrong-enrollment": |
| o.Claim.Op.Intent["source_enrollment"] = strings.Repeat("d", 32) |
| case "wrong-region": |
| - o.Claim.Op.Intent["source_region"] = "uk" |
| + guard = &c7Guard{region: "uk"} |
| case "foreign-exec": |
| o.Claim.Op.Exec = "target" |
| case "journal-rebind": |
| @@ -153,8 +158,10 @@ func TestC7ConversionRefusesWithoutWriting(t *testing.T) { |
| ref.SHA256 = strings.Repeat("0", 64) |
| case "no-guard": |
| guard = nil |
| - case "missing-admission": |
| - delete(o.Claim.Op.Intent, "admission_id") |
| + case "missing-enrollment": |
| + delete(o.Claim.Op.Intent, "source_enrollment") |
| + case "missing-source": |
| + delete(o.Claim.Op.Intent, "source") |
| } |
| raw, _ := json.Marshal(o.Claim) |
| if _, err := ConvertMovingToRelocating(context.Background(), s, o, id, ref, a, guard); err == nil { |
| @@ -212,7 +219,8 @@ func TestC7RelocatingPhaseValidation(t *testing.T) { |
| _, o, _, _ := c7Fixture(t) |
| c := o.Claim |
| c.Op.Kind, c.Op.Phase = KindRelocating, phase |
| - c.Op.Intent["region"] = "ie" |
| + // A relocating op's own intent (region HA's shape), not a move's. |
| + c.Op.Intent = map[string]string{"region": "ie", "source_box": "adi-knitting", "target_box": "bri-knitting"} |
| switch phase { |
| case "starting", "releasing": |
| c.Op.Exec = "adi-knitting" |
| @@ -249,3 +257,31 @@ func TestC7JournalFenceCannotNameAnotherBox(t *testing.T) { |
| t.Fatal("journal key rebound to another box") |
| } |
| } |
| + |
| +// The C7 contract with moves: a claim BeginMove produced, plus the source |
| +// enrollment the request door records, converts. |
| +func TestC7ConvertsAClaimBeginMoveProduced(t *testing.T) { |
| + s, o, ref, a := c7Fixture(t) |
| + ctx := context.Background() |
| + var err error |
| + o, err = Update(ctx, s, o, func(c *Claim) error { |
| + c.Op = Op{Kind: KindServing, Exec: "adi-knitting"} |
| + c.Obligations = nil |
| + if err := BeginMove("move-1", "member", "adi-knitting", "bri-knitting", "uk", time.Now())(c); err != nil { |
| + return err |
| + } |
| + c.Op.Intent["source_enrollment"] = ref.Enrollment |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + s.puts = 0 |
| + after, err := ConvertMovingToRelocating(ctx, s, o, "move-1", ref, a, &c7Guard{}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if after.Claim.Op.Kind != KindRelocating || after.Claim.Op.Phase != "pinning" || after.Claim.Op.Intent["source_box"] != "adi-knitting" { |
| + t.Fatalf("not converted: %+v", after.Claim.Op) |
| + } |
| +} |