| index 66153a317..7280b2ea5 100644 |
| --- a/internal/hostclaim/claim.go |
| +++ b/internal/hostclaim/claim.go |
| @@ -105,15 +105,23 @@ type InventoryEntry struct { |
| Paths map[string]uint64 `json:"paths"` // path -> stamp |
| } |
| |
| +type ParkScope struct { |
| + Location string `json:"location"` |
| + Incarnation string `json:"incarnation"` |
| + EvidenceAt time.Time `json:"evidence_at"` |
| + RequestLimit time.Duration `json:"request_limit_ns"` |
| +} |
| + |
| type Obligation struct { |
| - ID string `json:"id"` |
| - Op string `json:"op"` |
| - Kind string `json:"kind"` // "park-replica" | "park-local" | "park-generation" |
| - Region string `json:"region,omitempty"` |
| - Generation string `json:"generation,omitempty"` |
| - Box string `json:"box,omitempty"` |
| - Retirement string `json:"retirement"` |
| - Owner string `json:"owner"` // "region:<r>" | "box:<b>" |
| + Park *ParkScope `json:"park,omitempty"` |
| + ID string `json:"id"` |
| + Op string `json:"op"` |
| + Kind string `json:"kind"` // "park-replica" | "park-local" | "park-generation" |
| + Region string `json:"region,omitempty"` |
| + Generation string `json:"generation,omitempty"` |
| + Box string `json:"box,omitempty"` |
| + Retirement string `json:"retirement"` |
| + Owner string `json:"owner"` // "region:<r>" | "box:<b>" |
| } |
| |
| type CancelledCreate struct { |
| @@ -199,10 +207,10 @@ func (c *Claim) Validate() error { |
| return fmt.Errorf("%w: op %q without id", ErrMalformed, c.Op.Kind) |
| } |
| if c.Holder == nil { |
| - if c.Op.Kind != "" && c.Op.Kind != KindPurging { // a purge fence has no holder (CC21) |
| + if c.Op.Kind != "" && c.Op.Kind != KindPurging && !holderlessSweep(c) { // a purge fence has no holder (CC21) |
| return fmt.Errorf("%w: op %q without holder", ErrMalformed, c.Op.Kind) |
| } |
| - if len(c.Obligations) > 0 { |
| + if len(c.Obligations) > 0 && !holderlessSweep(c) { |
| return fmt.Errorf("%w: a free claim with outstanding obligations", ErrMalformed) |
| } |
| } else { |
| @@ -238,6 +246,16 @@ func (c *Claim) Validate() error { |
| if o.ID == "" || o.Op == "" || o.Retirement == "" || seen[o.ID] { |
| return fmt.Errorf("%w: obligation ids", ErrMalformed) |
| } |
| + if o.Park != nil { |
| + p := o.Park |
| + if (o.Kind != "park-replica" && o.Kind != "park-generation") || !isHex64(p.Incarnation) || p.EvidenceAt.IsZero() || p.RequestLimit <= 0 || p.RequestLimit > time.Duration(1<<63-1)-15*time.Minute { |
| + return fmt.Errorf("%w: park scope", ErrMalformed) |
| + } |
| + base := "instances/" + c.Host |
| + if p.Location != base && p.Location != base+"/"+p.Incarnation { |
| + return fmt.Errorf("%w: park location", ErrMalformed) |
| + } |
| + } |
| seen[o.ID] = true |
| switch o.Kind { |
| case "park-generation": |
| @@ -258,3 +276,15 @@ func (c *Claim) Validate() error { |
| } |
| return nil |
| } |
| + |
| +func holderlessSweep(c *Claim) bool { |
| + if c.Holder != nil || c.Op.Kind != KindParking || c.Op.ID == "" || c.Op.Intent["park_sweep"] != "v1" || c.Op.Exec != "" || len(c.Obligations) == 0 { |
| + return false |
| + } |
| + for _, o := range c.Obligations { |
| + if o.Op != c.Op.ID || o.Park == nil || (o.Kind != "park-replica" && o.Kind != "park-generation") { |
| + return false |
| + } |
| + } |
| + return true |
| +} |