| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "path/filepath" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/activator" |
| 12 | "github.com/carlosframework/platform/internal/cgroup" |
| 13 | "github.com/carlosframework/platform/internal/hostbox" |
| 14 | "github.com/carlosframework/platform/internal/hostclaim" |
| 15 | "github.com/carlosframework/platform/internal/hostguard" |
| 16 | "github.com/carlosframework/platform/internal/hostwrite" |
| 17 | "github.com/carlosframework/platform/internal/provision" |
| 18 | "github.com/carlosframework/platform/internal/release" |
| 19 | ) |
| 20 | |
| 21 | type ClaimRoleAssembly struct { |
| 22 | Guard hostguard.Guard |
| 23 | Claims release.Store |
| 24 | // Root is the reservation-bearing isolated root launch protocol (claim |
| 25 | // Plan 11, R10). Nil keeps every claimed launch refused at Preflight. |
| 26 | prepareGroup func(string, cgroup.Limit) (*cgroup.Prepared, hostbox.LaunchGroup, error) |
| 27 | Root activator.RootRoleLauncher |
| 28 | Manager *cgroup.Manager |
| 29 | Journal *hostbox.LaunchJournal |
| 30 | Inventory *hostbox.StopInventoryDB |
| 31 | ManagedRoots map[string]string |
| 32 | Identity hostbox.IdentityProvider |
| 33 | BootID, ControllerID string |
| 34 | } |
| 35 | |
| 36 | func claimContainmentName(target hostguard.FenceTarget) string { |
| 37 | identity := struct { |
| 38 | Host, Incarnation string |
| 39 | Stamp, Session uint64 |
| 40 | Box, Enrollment, Boot, Controller string |
| 41 | }{target.Host, target.Incarnation, target.Stamp, target.Session, target.Box, target.Enrollment, target.BootID, target.ControllerID} |
| 42 | raw, _ := json.Marshal(identity) |
| 43 | sum := sha256.Sum256(raw) |
| 44 | return cgroup.ClaimContainmentName(hex.EncodeToString(sum[:24])) // the one format (merge item 8) |
| 45 | } |
| 46 | |
| 47 | // launchTarget binds the observed session to this controller's verified tuple. |
| 48 | func (a *ClaimRoleAssembly) launchTarget(ctx context.Context, o hostclaim.Observed) (hostguard.FenceTarget, error) { |
| 49 | if a == nil || a.BootID == "" || a.ControllerID == "" || o.Claim == nil || o.Claim.Holder == nil { |
| 50 | return hostguard.FenceTarget{}, hostwrite.ErrUnavailable |
| 51 | } |
| 52 | actor, err := a.Identity.Verify(ctx) |
| 53 | if err != nil { |
| 54 | return hostguard.FenceTarget{}, err |
| 55 | } |
| 56 | h := o.Claim.Holder |
| 57 | return hostguard.FenceTarget{Region: actor.Region, Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Op: o.Claim.Op.ID, Phase: o.Claim.Op.Phase, Box: actor.Box, Enrollment: actor.Enrollment, BootID: a.BootID, ControllerID: a.ControllerID, ClaimETag: o.ETag, DirectoryRevision: actor.Binding, RosterRevision: actor.Revision}, nil |
| 58 | } |
| 59 | |
| 60 | // Preflight admits a launch before retry reservation, admission or state |
| 61 | // preparation only while root speaks the complete isolated launch protocol. |
| 62 | // Every other answer, including no answer, is the isolation refusal. |
| 63 | func (a *ClaimRoleAssembly) Preflight(ctx context.Context, _ ClaimLaunchInputs) error { |
| 64 | if a == nil || a.Root == nil { |
| 65 | return activator.ErrClaimIsolationUnavailable |
| 66 | } |
| 67 | protocol, err := a.Root.Capability(ctx) |
| 68 | if err != nil { |
| 69 | return errors.Join(activator.ErrClaimIsolationUnavailable, err) |
| 70 | } |
| 71 | if protocol != hostbox.RoleLaunchProtocol { |
| 72 | return activator.ErrClaimIsolationUnavailable |
| 73 | } |
| 74 | return nil |
| 75 | } |
| 76 | |
| 77 | // PreflightLaunch refuses, before admission, selected inputs the isolated |
| 78 | // unit cannot carry. The runtime's prerequisites call it after Select. |
| 79 | func (a *ClaimRoleAssembly) PreflightLaunch(host string, in ClaimLaunchInputs) error { |
| 80 | if a == nil || a.Root == nil { |
| 81 | return activator.ErrClaimIsolationUnavailable |
| 82 | } |
| 83 | if a.ManagedRoots["data"] != "/data" { |
| 84 | return hostbox.ErrRoleLaunchShape |
| 85 | } |
| 86 | if len(in.Roles) != 1 || in.Roles[0] != "main" { |
| 87 | return hostbox.ErrUnsupported |
| 88 | } |
| 89 | if !filepath.IsAbs(in.Executable) || filepath.Clean(in.Executable) != in.Executable || !claimHexDigest(in.ExecutableSHA256) || in.Identity.Config == "" || in.Identity.Release == "" { |
| 90 | return hostbox.ErrRoleLaunchShape |
| 91 | } |
| 92 | paths, err := claimIsolatedPaths(a.ManagedRoots, host, in) |
| 93 | if err != nil { |
| 94 | return err |
| 95 | } |
| 96 | if in.Unit != nil { |
| 97 | return claimBoundInputMatch(host, in) |
| 98 | } |
| 99 | // The same validator startIsolated runs on the built command. |
| 100 | _, socket, db, err := activator.IsolatedArgv(host, paths.LaunchDir, paths.RunDir, in.Args, in.Env) |
| 101 | if err != nil { |
| 102 | return err |
| 103 | } |
| 104 | if db != in.DBPath || socket != provision.NestedSocket(host) { |
| 105 | return hostbox.ErrRoleLaunchShape |
| 106 | } |
| 107 | return nil |
| 108 | } |
| 109 | |
| 110 | // claimIsolatedLaunch is where main's isolated run unit finds a host: its |
| 111 | // instance directory (the only data the unit binds), its run directory, its |
| 112 | // stdout socket and its slice tier. |
| 113 | type claimIsolatedLaunch struct { |
| 114 | LaunchDir, RunDir, LogSocket string |
| 115 | MemoryMax int64 |
| 116 | } |
| 117 | |
| 118 | // claimIsolatedPaths admits only the nested layout's database, directly in |
| 119 | // the host's instance directory, and a platform tier. |
| 120 | func claimIsolatedPaths(roots map[string]string, host string, in ClaimLaunchInputs) (claimIsolatedLaunch, error) { |
| 121 | data := roots["data"] |
| 122 | if !filepath.IsAbs(data) || !provision.ValidHostShape(host) || !filepath.IsAbs(in.DBPath) || filepath.Clean(in.DBPath) != in.DBPath || |
| 123 | (in.Limit.MemoryMax != cgroup.TierDefault.MemoryMax && in.Limit.MemoryMax != cgroup.TierChildren.MemoryMax) { |
| 124 | return claimIsolatedLaunch{}, hostbox.ErrRoleLaunchShape |
| 125 | } |
| 126 | dir := provision.InstanceDir(data, host) |
| 127 | if filepath.Dir(in.DBPath) != dir { |
| 128 | return claimIsolatedLaunch{}, hostbox.ErrRoleLaunchShape |
| 129 | } |
| 130 | return claimIsolatedLaunch{LaunchDir: dir, RunDir: provision.InstanceRunDir(host), LogSocket: provision.LogSocket(host, "run"), MemoryMax: in.Limit.MemoryMax}, nil |
| 131 | } |
| 132 | |
| 133 | // PreflightKind checks root's installed verifier before any attempt/admission. |
| 134 | // The existing RootRoleLauncher signatures remain unchanged. |
| 135 | func (a *ClaimRoleAssembly) PreflightKind(ctx context.Context, kind string) error { |
| 136 | if kind == hostclaim.KindServing { |
| 137 | return nil |
| 138 | } |
| 139 | client, ok := a.Root.(interface { |
| 140 | KindCapability(context.Context, string) error |
| 141 | }) |
| 142 | if !ok { |
| 143 | return activator.ErrClaimIsolationUnavailable |
| 144 | } |
| 145 | return client.KindCapability(ctx, kind) |
| 146 | } |
| 147 | |