| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/caps" |
| 9 | "github.com/carlosframework/platform/internal/controlsock" |
| 10 | "github.com/carlosframework/platform/internal/hostbox" |
| 11 | "github.com/carlosframework/platform/internal/hostclaim" |
| 12 | "github.com/carlosframework/platform/internal/hostwrite" |
| 13 | "github.com/carlosframework/platform/internal/registry" |
| 14 | "github.com/carlosframework/platform/internal/rootstate" |
| 15 | ) |
| 16 | |
| 17 | var openClaimUnitBindingsRead = func() (rootstate.Store, error) { |
| 18 | return rootstate.Open("/var/lib/carlos-claims/unit-bindings", false, false) |
| 19 | } |
| 20 | |
| 21 | type claimUnitShapeReader func(context.Context, registry.Route, hostbox.Selection) error |
| 22 | |
| 23 | func claimUnitShapePolicy(route func(string) (*registry.Route, error), read claimUnitShapeReader) hostbox.PolicyRefusal { |
| 24 | return func(ctx context.Context, _ *hostclaim.Claim, s hostbox.Selection, _ hostwrite.Identity) error { |
| 25 | return claimUnitShapeCheck(ctx, route, read, s, s.Installation()) |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // claimUnitShapeCheck is the unit-shape refusal's decision for selection s. |
| 30 | // installation is s.Installation(): whether s is the work an installation |
| 31 | // selected for itself (hostbox's installWithWork), the only work that may |
| 32 | // go on without a registry row. |
| 33 | func claimUnitShapeCheck(ctx context.Context, route func(string) (*registry.Route, error), read claimUnitShapeReader, s hostbox.Selection, installation bool) error { |
| 34 | if route == nil || read == nil { |
| 35 | return os.ErrPermission |
| 36 | } |
| 37 | rt, err := route(s.Host) |
| 38 | if err != nil { |
| 39 | return err |
| 40 | } |
| 41 | if rt == nil { |
| 42 | // A fresh install has no row until registerRemote's second |
| 43 | // work.With writes it, and every work.With before that runs this |
| 44 | // policy (harness G10, 2026-10-21). Only the installation's own work |
| 45 | // may go on without a row. Everything else on a host whose row is |
| 46 | // absent (a timer delivering to its cached route, a restart, a |
| 47 | // retired or deleted host) refuses as before (fix round 2). The |
| 48 | // installation checks an empty route for the host, as |
| 49 | // rootClaimRestartShape does: a host with a configured shape or a |
| 50 | // saved unit binding still refuses through checkClaimUnitShape |
| 51 | // (root's boundInputs in agent mode), and registerRemote refuses to |
| 52 | // recreate a row its inventory already records as materialized. |
| 53 | if !installation { |
| 54 | return hostbox.ErrChanged |
| 55 | } |
| 56 | rt = ®istry.Route{Host: s.Host} |
| 57 | } |
| 58 | if rt.Host != s.Host { |
| 59 | return hostbox.ErrChanged |
| 60 | } |
| 61 | return read(ctx, *rt, s) |
| 62 | } |
| 63 | |
| 64 | func checkClaimUnitShape(ctx context.Context, b *claimUnitBindings, shapes map[string]claimUnitShape, rt registry.Route, s hostbox.Selection) error { |
| 65 | saved, err := b.read(rt.Host) |
| 66 | if errors.Is(err, os.ErrNotExist) { |
| 67 | if _, configured := shapes[rt.Host]; configured || rt.Unit != "" || rt.Backing != "" || rt.Tunnel != "" { |
| 68 | return hostbox.ErrUnsupported |
| 69 | } |
| 70 | return nil |
| 71 | } |
| 72 | if err != nil { |
| 73 | return err |
| 74 | } |
| 75 | if saved.Retiring || saved.Retired || saved.Shape != shapes[rt.Host] || !saved.Shape.matches(rt) { |
| 76 | return hostbox.ErrChanged |
| 77 | } |
| 78 | if s.Host != "" && (saved.Owner.Host != s.Host || saved.Owner.Box != s.Box || saved.Owner.Incarnation != s.Captured.Incarnation || saved.Owner.Stamp != s.Captured.Stamp) { |
| 79 | return hostbox.ErrChanged |
| 80 | } |
| 81 | return ctx.Err() |
| 82 | } |
| 83 | |
| 84 | func withClaimUnitShape(ctx context.Context, rt registry.Route, s hostbox.Selection, fn func(claimUnitBinding) error) error { |
| 85 | state, err := openClaimUnitBindingsRead() |
| 86 | shapes, configErr := productionClaimUnitShapes() |
| 87 | if errors.Is(err, os.ErrNotExist) && rt.Unit == "" && rt.Backing == "" && rt.Tunnel == "" && shapes[rt.Host].Host == "" { |
| 88 | return fn(claimUnitBinding{}) |
| 89 | } |
| 90 | if err != nil { |
| 91 | return err |
| 92 | } |
| 93 | defer state.Close() |
| 94 | b := &claimUnitBindings{state: state} |
| 95 | if configErr != nil { |
| 96 | _, e := b.read(rt.Host) |
| 97 | if !errors.Is(e, os.ErrNotExist) || rt.Unit != "" || rt.Backing != "" || rt.Tunnel != "" { |
| 98 | return configErr |
| 99 | } |
| 100 | shapes = map[string]claimUnitShape{} |
| 101 | } |
| 102 | if err = checkClaimUnitShape(ctx, b, shapes, rt, s); err != nil { |
| 103 | return err |
| 104 | } |
| 105 | saved, err := b.read(rt.Host) |
| 106 | if errors.Is(err, os.ErrNotExist) { |
| 107 | return fn(claimUnitBinding{}) |
| 108 | } |
| 109 | if err != nil { |
| 110 | return err |
| 111 | } |
| 112 | return fn(saved) |
| 113 | } |
| 114 | |
| 115 | func productionClaimUnitShapePolicy(mode string, route func(string) (*registry.Route, error)) hostbox.PolicyRefusal { |
| 116 | return claimUnitShapePolicy(route, func(ctx context.Context, rt registry.Route, s hostbox.Selection) error { |
| 117 | if mode == "agent" { |
| 118 | _, err := NewClaimRoleRootClient(controlsock.Path).UnitInputs(ctx, rt) |
| 119 | return err |
| 120 | } |
| 121 | if mode != "adopt" && mode != "instance-control" { |
| 122 | return os.ErrPermission |
| 123 | } |
| 124 | return withClaimUnitShape(ctx, rt, s, func(claimUnitBinding) error { return nil }) |
| 125 | }) |
| 126 | } |
| 127 | |
| 128 | func rootClaimUnitInputs(ctx context.Context, rt registry.Route) (*hostbox.UnitLaunchInputs, error) { |
| 129 | var out *hostbox.UnitLaunchInputs |
| 130 | err := withClaimUnitShape(ctx, rt, hostbox.Selection{}, func(b claimUnitBinding) error { |
| 131 | if b == (claimUnitBinding{}) { |
| 132 | return nil |
| 133 | } |
| 134 | p, err := readClaimUnitProgram(b.Shape) |
| 135 | if err != nil { |
| 136 | return err |
| 137 | } |
| 138 | out = &hostbox.UnitLaunchInputs{Unit: b.Shape.Unit, Socket: b.Shape.Socket, Addr: b.Shape.Addr, DBPath: b.Shape.DBPath, Program: b.Shape.Program, Args: p.Args, Env: p.Env} |
| 139 | return nil |
| 140 | }) |
| 141 | return out, err |
| 142 | } |
| 143 | |
| 144 | func rootClaimRestartShape(ctx context.Context, host string, rt *registry.Route) error { |
| 145 | if rt == nil { |
| 146 | rt = ®istry.Route{Host: host} |
| 147 | } |
| 148 | if rt.Host != host { |
| 149 | return os.ErrPermission |
| 150 | } |
| 151 | return withClaimUnitShape(ctx, *rt, hostbox.Selection{}, func(b claimUnitBinding) error { |
| 152 | if b != (claimUnitBinding{}) { |
| 153 | return hostbox.ErrUnsupported |
| 154 | } |
| 155 | return nil |
| 156 | }) |
| 157 | } |
| 158 | |
| 159 | func (c rootRestartChecks) checkUnitShape(ctx context.Context, host string, rt *registry.Route) error { |
| 160 | if c.unitShape == nil { |
| 161 | return nil |
| 162 | } |
| 163 | legacy, err := c.allowLegacy() |
| 164 | if err != nil { |
| 165 | return err |
| 166 | } |
| 167 | if legacy { |
| 168 | return nil |
| 169 | } |
| 170 | return c.unitShape(ctx, host, rt) |
| 171 | } |
| 172 | |
| 173 | func productionClaimCapsOps(o caps.Ops, census func() (bool, error)) caps.Ops { |
| 174 | previous := o.CheckHost |
| 175 | o.CheckHost = func(host string) error { |
| 176 | if census == nil { |
| 177 | return os.ErrPermission |
| 178 | } |
| 179 | legacy, err := census() |
| 180 | if err != nil { |
| 181 | return err |
| 182 | } |
| 183 | if !legacy { |
| 184 | if err := claimBoundCapsRefusal(host); err != nil { |
| 185 | return err |
| 186 | } |
| 187 | } |
| 188 | if previous != nil { |
| 189 | return previous(host) |
| 190 | } |
| 191 | return nil |
| 192 | } |
| 193 | return o |
| 194 | } |
| 195 | |
| 196 | func claimBoundCapsRefusal(host string) error { |
| 197 | shapes, e := productionClaimUnitShapes() |
| 198 | if e == nil { |
| 199 | if _, bound := shapes[host]; bound { |
| 200 | return hostbox.ErrUnsupported |
| 201 | } |
| 202 | } |
| 203 | state, e := openClaimUnitBindingsRead() |
| 204 | if e != nil && !errors.Is(e, os.ErrNotExist) { |
| 205 | return e |
| 206 | } |
| 207 | if e == nil { |
| 208 | b := &claimUnitBindings{state: state} |
| 209 | _, e = b.read(host) |
| 210 | state.Close() |
| 211 | if e == nil { |
| 212 | return hostbox.ErrUnsupported |
| 213 | } |
| 214 | if !errors.Is(e, os.ErrNotExist) { |
| 215 | return e |
| 216 | } |
| 217 | } |
| 218 | return nil |
| 219 | } |
| 220 | |