package hostclaim import ( "encoding/json" "errors" "fmt" "strings" "time" ) var ErrAckAlreadyApplied = errors.New("hostclaim: execution acknowledgement already applied") type ExecutionAcknowledgement struct { Op string `json:"op"` Enrollment string `json:"enrollment"` BootID string `json:"boot_id"` ControllerID string `json:"controller_id"` LeaseETag string `json:"lease_etag"` Expires time.Time `json:"expires"` } // AcknowledgeExecution records the acknowledged lease for one required // participant. The lease write and durable local state happen before this // transition. The first acknowledgement binds the participant's boot and // controller, and later ones only extend the same lease: a replacement must // carry the same op, enrollment, boot and controller, a lease ETag and a // strictly later expiry. Anything else is a conflicting acknowledgement and // is never overwritten. func AcknowledgeExecution(box string, ack ExecutionAcknowledgement) func(*Claim) error { return func(c *Claim) error { if !validBoxReference(box) || c.Op.Kind == "" || c.Op.Kind == KindServing || c.Op.ID == "" || ack.Op != c.Op.ID || ack.Enrollment == "" || ack.BootID == "" || ack.ControllerID == "" || ack.LeaseETag == "" || ack.Expires.IsZero() || !stringPresent(c.Op.Participants, box) { return fmt.Errorf("%w: execution acknowledgement is not for a required active participant", ErrWrongState) } raw, err := json.Marshal(ack) if err != nil { return err } if c.Op.Journal == nil { c.Op.Journal = map[string]string{} } key := "exec_ack." + box if previous, ok := c.Op.Journal[key]; ok { if previous == string(raw) { return ErrAckAlreadyApplied } if !extendsAcknowledgement(previous, ack) { return fmt.Errorf("%w: participant already acknowledged different lease", ErrWrongState) } } c.Op.Journal[key] = string(raw) return nil } } // extendsAcknowledgement reports whether next only extends the lease that // previous acknowledged: the same op, enrollment, boot and controller, a lease // ETag, and a strictly later expiry. An undecodable previous value never is. func extendsAcknowledgement(previous string, next ExecutionAcknowledgement) bool { dec := json.NewDecoder(strings.NewReader(previous)) dec.DisallowUnknownFields() var prior ExecutionAcknowledgement if dec.Decode(&prior) != nil || dec.More() { return false } return prior.Op == next.Op && prior.Enrollment == next.Enrollment && prior.BootID == next.BootID && prior.ControllerID == next.ControllerID && next.LeaseETag != "" && next.Expires.After(prior.Expires) } func validBoxReference(box string) bool { if box == "" || len(box) > 128 || strings.ContainsAny(box, "/\\\x00\r\n\t ") { return false } return true }