package hostfence import ( "context" "encoding/json" "errors" "github.com/carlosframework/platform/internal/awsinv" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/termfence" "time" ) type Worker = termfence.WrittenBy type Termination = termfence.Record type EvidenceRef struct { Enrollment string `json:"enrollment"` Key string `json:"key"` SHA256 string `json:"sha256"` } type TerminationWorker struct { Fences release.Store Enabled bool Directory hostactors.Directory Self Worker Reader func(context.Context, string) (awsinv.ExactEC2API, error) Now func() time.Time } func FenceKey(enrollment string) string { return "control/box-fences/" + enrollment + ".json" } func (w TerminationWorker) binding(ctx context.Context, box string) (hostactors.Box, error) { if !w.Enabled || w.Now == nil || w.Fences == nil { return hostactors.Box{}, ErrObservation } o, err := w.Directory.Read(ctx) if err != nil { return hostactors.Box{}, err } self, ok := o.Roster.Boxes[w.Self.Box] if !ok || self.State != "active" || self.Enrollment != w.Self.Enrollment { return hostactors.Box{}, ErrObservation } eligible, err := w.Directory.Lookup(ctx, w.Self.Box) if err != nil || eligible.State != "active" || eligible.Enrollment != w.Self.Enrollment { return hostactors.Box{}, ErrObservation } target, ok := o.Roster.Boxes[box] if !ok || target.Region != self.Region || target.Cloud.Region != self.Cloud.Region || target.Cloud.Account != self.Cloud.Account { return hostactors.Box{}, ErrObservation } return target, nil } func (w TerminationWorker) Describe(ctx context.Context, box string) (EvidenceRef, error) { b, err := w.binding(ctx, box) if err != nil { return EvidenceRef{}, err } if w.Reader == nil { return EvidenceRef{}, ErrObservation } api, err := w.Reader(ctx, b.Cloud.Region) if err != nil { return EvidenceRef{}, err } out, err := termfence.DescribeInstance(ctx, api, b.Cloud) if err != nil { return EvidenceRef{}, err } return w.verifyAndPersist(ctx, box, b, termfence.FromDescribe(out, b.Cloud.InstanceID, b.Cloud.Region)) } func (w TerminationWorker) verifyAndPersist(ctx context.Context, box string, b hostactors.Box, observation termfence.Observation) (EvidenceRef, error) { o, err := w.Directory.Read(ctx) if err != nil { return EvidenceRef{}, err } self, ok := o.Roster.Boxes[w.Self.Box] if !ok || self.State != "active" || self.Enrollment != w.Self.Enrollment { return EvidenceRef{}, ErrObservation } f, err := termfence.Verify(termfence.Box{Ref: box, Enrollment: b.Enrollment, Cloud: b.Cloud}, termfence.Box{Ref: w.Self.Box, Enrollment: w.Self.Enrollment, Cloud: self.Cloud}, observation, w.Now()) if err != nil { return EvidenceRef{}, err } f.ObservedAt = w.Now().UTC().Format(time.RFC3339Nano) if err := hostactors.ValidateTerminationFence(o, box, f, w.Now()); err != nil { return EvidenceRef{}, err } return w.persist(ctx, f) } // Event is called only by the regional queue consumer, after the existing // queue URL and rule-ARN policy validation. It is never an HTTP request body. func (w TerminationWorker) Event(ctx context.Context, box string, raw []byte) (EvidenceRef, error) { b, err := w.binding(ctx, box) if err != nil { return EvidenceRef{}, err } observation, err := termfence.FromEvent(raw) if err != nil { return EvidenceRef{}, err } var event struct { Time time.Time `json:"time"` } if json.Unmarshal(raw, &event) != nil || event.Time.IsZero() || event.Time.After(w.Now().Add(5*time.Second)) { return EvidenceRef{}, ErrObservation } return w.verifyAndPersist(ctx, box, b, observation) } func (w TerminationWorker) persist(ctx context.Context, f Termination) (EvidenceRef, error) { raw, err := json.Marshal(f) if err != nil { return EvidenceRef{}, err } key := FenceKey(f.Enrollment) _, err = w.Fences.Put(ctx, key, raw, "") if err != nil { saved, _, readErr := w.Fences.Get(ctx, key) if readErr != nil { return EvidenceRef{}, errors.Join(err, readErr) } var old Termination o, readErr := w.Directory.Read(ctx) if readErr != nil { return EvidenceRef{}, readErr } if json.Unmarshal(saved, &old) != nil || old.Enrollment != f.Enrollment || old.Cloud != f.Cloud || w.Directory.ValidateTerminationFenceAuthority(ctx, o, f.Box, old, w.Now()) != nil { return EvidenceRef{}, ErrObservation } raw = saved } return EvidenceRef{Enrollment: f.Enrollment, Key: key, SHA256: hostfloor.Digest(raw)}, nil }