package host import ( "bytes" "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "os" "github.com/carlosframework/platform/internal/rootstate" ) type claimUnitMaterialization struct { Migration *claimUnitMigration Prior claimUnitBinding PriorIdentity rootstate.FileIdentity Binding claimUnitBinding Digest string Identity rootstate.FileIdentity Retiring, Retired bool } type claimUnitMaterializer struct { admit func() error socketCapture func(context.Context, claimUnitShape, claimUnitPredecessor) (claimUnitSocketIdentity, error) socketPark func(claimUnitBinding, claimUnitSocketIdentity) error predecessor func(context.Context, claimUnitBinding) (claimUnitPredecessor, error) stopPredecessor func(context.Context, claimUnitPredecessor) error idle func(string) error state rootstate.Store read func(string) ([]byte, rootstate.FileIdentity, error) create func(string, []byte) error replace func(string, rootstate.FileIdentity, []byte) error } func claimUnitDigest(b []byte) string { sum := sha256.Sum256(b); return hex.EncodeToString(sum[:]) } func (m claimUnitMaterializer) load(host string) (claimUnitMaterialization, error) { var r claimUnitMaterialization raw, err := m.state.Read(host + ".unit.json") if err != nil { return r, err } dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() if dec.Decode(&r) != nil || dec.Decode(new(any)) != io.EOF || !r.Binding.valid() || r.Binding.Owner.Host != host || !claimHexDigest(r.Digest) || (r.Retired && !r.Retiring) { return r, os.ErrPermission } if r.Migration != nil && (!r.Migration.valid(r.Binding) || r.Prior != (claimUnitBinding{})) { return r, os.ErrPermission } if r.Prior != (claimUnitBinding{}) && (!r.Prior.valid() || !r.Prior.Retired || r.Prior.Shape != r.Binding.Shape || r.Prior.Owner == r.Binding.Owner || r.PriorIdentity == (rootstate.FileIdentity{})) { return r, os.ErrPermission } return r, nil } func (m claimUnitMaterializer) save(r claimUnitMaterialization) error { raw, err := json.Marshal(r) if err != nil { return err } return m.state.Write(r.Binding.Owner.Host+".unit.json", raw) } func claimOwnedUnitDefinition(b claimUnitBinding) ([]byte, error) { raw, err := claimBoundUnitDefinition(b.Shape) if err != nil { return nil, err } owner, _ := json.Marshal(b.Owner) return append([]byte("# claim-owner="+claimUnitDigest(owner)+"\n"), raw...), nil } func claimRetiredUnitDefinition(b claimUnitBinding) []byte { owner, _ := json.Marshal(b.Owner) return []byte("# claim-retired=" + claimUnitDigest(owner) + "\n[Unit]\nRefuseManualStart=yes\n[Service]\nType=oneshot\nExecStart=/usr/bin/false\nRestart=no\n") } func (m claimUnitMaterializer) install(ctx context.Context, b claimUnitBinding) error { if m.admit != nil { if err := m.admit(); err != nil { return err } } if m.state == nil || m.read == nil || m.create == nil || m.idle == nil || !b.valid() || b.Retiring || b.Retired { return os.ErrPermission } unlock, err := m.state.Lock(ctx, "materialize") if err != nil { return err } defer unlock() want, err := claimOwnedUnitDefinition(b) if err != nil { return err } r, err := m.load(b.Owner.Host) if errors.Is(err, os.ErrNotExist) { raw, id, e := m.read(b.Shape.Unit) r = claimUnitMaterialization{Binding: b, Digest: claimUnitDigest(want)} if e == nil { r.Migration, err = m.migration(ctx, b, raw, id) if err != nil { return err } } else if !errors.Is(e, os.ErrNotExist) { return e } } else if err != nil { return err } if r.Binding != b || r.Digest != claimUnitDigest(want) || r.Retiring || r.Retired { return os.ErrPermission } if r.Identity == (rootstate.FileIdentity{}) && r.Migration == nil { if err := m.idle(b.Shape.Host); err != nil { return err } } if err = m.save(r); err != nil { return err } if r.Identity == (rootstate.FileIdentity{}) && r.Migration != nil { return m.finishMigration(ctx, r, want) } raw, id, err := m.read(b.Shape.Unit) if errors.Is(err, os.ErrNotExist) && r.Identity == (rootstate.FileIdentity{}) { if err = m.create(b.Shape.Unit, want); err != nil { return err } raw, id, err = m.read(b.Shape.Unit) } if err != nil { return err } if !bytes.Equal(raw, want) || id == (rootstate.FileIdentity{}) || (r.Identity != (rootstate.FileIdentity{}) && !r.Identity.Same(id)) { return os.ErrPermission } r.Identity = id return m.save(r) } func (m claimUnitMaterializer) verify(b claimUnitBinding) error { r, err := m.load(b.Owner.Host) if err != nil { return err } if r.Binding != b || r.Retiring || r.Retired || r.Identity == (rootstate.FileIdentity{}) { return os.ErrPermission } raw, id, err := m.read(b.Shape.Unit) if err != nil { return err } want, err := claimOwnedUnitDefinition(b) if err != nil { return err } if !id.Same(r.Identity) || !bytes.Equal(raw, want) || claimUnitDigest(raw) != r.Digest { return os.ErrPermission } return nil } func (m claimUnitMaterializer) retire(ctx context.Context, b claimUnitBinding, gone func() error) error { if m.state == nil || m.read == nil || m.replace == nil || gone == nil || !b.valid() { return os.ErrPermission } unlock, err := m.state.Lock(ctx, "materialize") if err != nil { return err } defer unlock() r, err := m.load(b.Owner.Host) if err != nil { return err } if r.Binding != b || r.Identity == (rootstate.FileIdentity{}) { return os.ErrPermission } raw, id, err := m.read(b.Shape.Unit) if err != nil { return err } tombstone := claimRetiredUnitDefinition(b) if r.Retiring && bytes.Equal(raw, tombstone) { if r.Retired && !r.Identity.Same(id) { return os.ErrPermission } r.Identity = id r.Retired = true return m.save(r) } if r.Retired || !id.Same(r.Identity) || claimUnitDigest(raw) != r.Digest { return os.ErrPermission } r.Retiring = true if err = m.save(r); err != nil { return err } if err = gone(); err != nil { return err } if err = m.replace(b.Shape.Unit, r.Identity, tombstone); err != nil { return err } raw, id, err = m.read(b.Shape.Unit) if err != nil { return err } if !bytes.Equal(raw, tombstone) { return os.ErrPermission } r.Identity = id r.Retired = true return m.save(r) }