package hostbox import ( "context" "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "fmt" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) func (a ReconcileAdapter) CaptureInventory(ctx context.Context, s RecordSelection, c hostguard.Captured, revision uint64) (InventoryExpectation, error) { obs, err := hostclaim.Read(ctx, a.Reconciler.Claims, s.Host) if err != nil { return InventoryExpectation{}, err } if obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil { return InventoryExpectation{}, ErrChanged } h := obs.Claim.Holder if h.Incarnation != c.Incarnation || h.Stamp != c.Stamp || h.Key != s.Key { return InventoryExpectation{}, ErrChanged } _, etag, err := a.Reconciler.Claims.Get(ctx, s.Key) if err != nil { return InventoryExpectation{}, err } if etag != s.ETag { return InventoryExpectation{}, ErrChanged } if revision != 0 { if err := a.inventoryRevision(ctx, s.Host, c, revision); err != nil { return InventoryExpectation{}, err } } return InventoryExpectation{Revision: revision, RecordKey: s.Key, RecordETag: etag, Target: hostguard.FenceTarget{Host: s.Host, Incarnation: c.Incarnation, Stamp: c.Stamp, Session: h.Session, Box: a.Box, Enrollment: a.Enrollment, ClaimETag: obs.ETag, Op: obs.Claim.Op.ID, Phase: obs.Claim.Op.Phase}}, nil } func (a ReconcileAdapter) inventoryRevision(ctx context.Context, host string, c hostguard.Captured, revision uint64) error { if a.Reconciler.Guard.FenceView == nil { return ErrUnsupported } view, err := a.Reconciler.Guard.FenceView(ctx, host, c) if err != nil { return err } if view.Schema != 1 || view.Host != host || view.Captured != c || view.Installation.InventoryRevision != revision { return ErrChanged } return nil } func (a ReconcileAdapter) RevalidateInventory(ctx context.Context, s RecordSelection, c hostguard.Captured, e InventoryExpectation, revision uint64) error { fresh, err := a.CaptureInventory(ctx, s, c, revision) if err != nil { return err } if fresh.Target != e.Target { return ErrChanged } return nil } func (a ReconcileAdapter) prepareRemoteInventory(ctx context.Context, s RecordSelection, c hostguard.Captured) error { _, revision, err := a.Inventory.Snapshot(ctx, s.Host, c) if err != nil { return err } if revision != 0 { return nil } var expected InventoryExpectation err = a.Reconciler.Guard.WithLocal(ctx, s.Host, func() error { var err error; expected, err = a.CaptureInventory(ctx, s, c, 0); return err }) if err != nil { return err } in := StopInventory{Complete: true, Host: s.Host, Incarnation: c.Incarnation, InstallationStamp: c.Stamp, Box: a.Box, Enrollment: a.Enrollment, DirectoryRevision: a.DirectoryRevision} revision, err = a.Inventory.WriteExpected(ctx, in, expected) if err != nil { return err } return a.Reconciler.Guard.WithLocal(ctx, s.Host, func() error { return a.RevalidateInventory(ctx, s, c, expected, revision) }) } func (a ReconcileAdapter) installRemote(ctx context.Context, rec provision.ClaimRecord, s RecordSelection, r Reconciler, write func(registry.ClaimOwnership) error) error { if a.Census != nil { row, err := a.Registry.Get(s.Host) if err != nil { return err } if row != nil && row.ClaimOwnership.Valid() { captured := hostguard.Captured{Incarnation: row.ClaimIncarnation, Stamp: row.ClaimStamp} in, revision, err := a.Inventory.Snapshot(ctx, s.Host, captured) if err != nil { return err } r.CheckHeld = func(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error { if obs.Claim == nil || obs.Claim.Holder == nil || obs.Host != selected.Host || obs.Claim.Holder.Incarnation != selected.Incarnation { return ErrChanged } current, err := a.Registry.Get(selected.Host) if err != nil { return err } if current == nil || current.ClaimOwnership != row.ClaimOwnership { return ErrChanged } if err := a.inventoryRevision(ctx, selected.Host, captured, revision); err != nil { return err } return a.Census.checkHeldInventory(selected, row.ClaimOwnership, in) } } else { r.CheckHeld = func(context.Context, hostclaim.Observed, RecordSelection) error { return ErrTypedInventory } } } r.CheckLocal = func(context.Context, hostclaim.Observed, RecordSelection) error { return nil } r.PrepareInventory = a.prepareRemoteInventory return r.installWithWork(ctx, s, func(work hostwrite.Work) error { captured, err := r.Guard.Capture(s.Host) if err != nil { return err } for attempt := 0; attempt < 3; attempt++ { err = a.registerRemote(ctx, s, captured, work, write) if !errors.Is(err, ErrChanged) { return err } } return ErrChanged }) } func (a ReconcileAdapter) registerRemote(ctx context.Context, s RecordSelection, c hostguard.Captured, work hostwrite.Work, write func(registry.ClaimOwnership) error) error { in, revision, err := a.Inventory.Snapshot(ctx, s.Host, c) if err != nil { return err } if !in.Complete || in.Host != s.Host || in.Incarnation != c.Incarnation || in.InstallationStamp != c.Stamp || in.Box != a.Box || in.Enrollment != a.Enrollment || in.DirectoryRevision != a.DirectoryRevision { return ErrTypedInventory } sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", s.Host, c.Incarnation, c.Stamp, a.Box))) owner := registry.ClaimOwnership{ClaimIncarnation: c.Incarnation, ClaimStamp: c.Stamp, ClaimOwner: hex.EncodeToString(sum[:])} index := -1 for i, e := range in.Entries { if e.Kind == "route" && e.RelativePath == s.Host { if index != -1 || e.Ownership != owner.ClaimOwner || e.RowIdentity == "" { return ErrTypedInventory } index = i owner.ClaimRowID = e.RowIdentity } } if index < 0 { var id [16]byte if _, err := rand.Read(id[:]); err != nil { return err } owner.ClaimRowID = hex.EncodeToString(id[:]) index = len(in.Entries) in.Entries = append(in.Entries, StopInventoryEntry{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: s.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "intended"}) var expected InventoryExpectation err = work.With(ctx, hostwrite.Identity{}, func() error { var err error; expected, err = a.CaptureInventory(ctx, s, c, revision); return err }) if err != nil { return err } revision, err = a.Inventory.WriteExpected(ctx, in, expected) if err != nil { return err } if err = work.With(ctx, hostwrite.Identity{}, func() error { return a.RevalidateInventory(ctx, s, c, expected, revision) }); err != nil { return err } } var expected InventoryExpectation err = work.With(ctx, hostwrite.Identity{}, func() error { var err error expected, err = a.CaptureInventory(ctx, s, c, revision) if err != nil { return err } row, err := a.Registry.Get(s.Host) if err != nil { return err } if row == nil && in.Entries[index].Materialization == "materialized" { return ErrTypedInventory } if row != nil && row.ClaimOwnership != owner { return registry.ErrClaimOwnership } if err = write(owner); err != nil { return err } row, err = a.Registry.Get(s.Host) if err != nil { return err } if row == nil || row.ClaimOwnership != owner { return registry.ErrClaimOwnership } return nil }) if err != nil { return err } if in.Entries[index].Materialization == "materialized" { return nil } in.Entries[index].Materialization = "materialized" revision, err = a.Inventory.WriteExpected(ctx, in, expected) if err != nil { return err } return work.With(ctx, hostwrite.Identity{}, func() error { return a.RevalidateInventory(ctx, s, c, expected, revision) }) } func (a ReconcileAdapter) PrepareInstallation(ctx context.Context, s RecordSelection, captured hostguard.Captured) error { if !a.Inventory.Remote() { return nil } return a.prepareRemoteInventory(ctx, s, captured) } func (a ReconcileAdapter) RegisterLanding(ctx context.Context, observed hostclaim.Observed) error { if !a.Inventory.Remote() || observed.Claim == nil || observed.Claim.Holder == nil { return ErrUnsupported } c := observed.Claim if c.Op.Kind != hostclaim.KindLanding || c.Op.Exec != a.Box || c.Op.Intent["target"] != a.Box || (c.Op.Phase != provision.LandChecking && c.Op.Phase != provision.LandConfigured) { return ErrChanged } h := c.Holder _, etag, err := a.Reconciler.Claims.Get(ctx, h.Key) if err != nil { return err } s := RecordSelection{Host: c.Host, Key: h.Key, ETag: etag, Incarnation: h.Incarnation, Family: h.Family, Layout: "flat"} captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp} in, rev, err := a.Inventory.Snapshot(ctx, c.Host, captured) if err != nil { return err } if rev == 0 { if c.Op.Phase != provision.LandChecking { return ErrChanged } in = StopInventory{Complete: true, Host: c.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: a.Box, Enrollment: a.Enrollment, DirectoryRevision: a.DirectoryRevision} } var expected InventoryExpectation if err = a.Reconciler.Guard.WithLocal(ctx, c.Host, func() error { var e error; expected, e = a.CaptureInventory(ctx, s, captured, rev); return e }); err != nil { return err } rev, err = a.Inventory.WriteExpected(ctx, in, expected) if err != nil { return err } if err = a.Reconciler.Guard.WithLocal(ctx, c.Host, func() error { return a.RevalidateInventory(ctx, s, captured, expected, rev) }); err != nil { return err } return nil }