| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "github.com/carlosframework/platform/internal/hostbox" |
| 7 | "log" |
| 8 | "os" |
| 9 | "sort" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/hostguard" |
| 12 | "github.com/carlosframework/platform/internal/instancectl" |
| 13 | "github.com/carlosframework/platform/internal/rootstate" |
| 14 | ) |
| 15 | |
| 16 | var openClaimUnitBindings = func() (rootstate.Store, error) { |
| 17 | return rootstate.Open("/var/lib/carlos-claims/unit-bindings", true, false) |
| 18 | } |
| 19 | |
| 20 | var openClaimUnitMaterializations = func() (rootstate.Store, error) { |
| 21 | return rootstate.Open("/var/lib/carlos-claims/unit-materializations", true, false) |
| 22 | } |
| 23 | |
| 24 | var productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { |
| 25 | shapes, err := readClaimUnitShapes("/etc/carlos/claim-units.json") |
| 26 | if errors.Is(err, os.ErrNotExist) { |
| 27 | return map[string]claimUnitShape{}, nil |
| 28 | } |
| 29 | return shapes, err |
| 30 | } |
| 31 | |
| 32 | // Called only in the census-gated root control startup. The root configuration |
| 33 | // chooses unit placement; the existing root installation chooses its owner. |
| 34 | func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *ClaimAssembly) error { |
| 35 | guard := h.Launch.Guard |
| 36 | if guard.Root == nil || guard.Claims == nil { |
| 37 | return os.ErrPermission |
| 38 | } |
| 39 | base, ok := h.Launch.Units.(instancectl.ClaimUnits) |
| 40 | if !ok { |
| 41 | return os.ErrPermission |
| 42 | } |
| 43 | shapes, configErr := productionClaimUnitShapes() |
| 44 | if configErr != nil { |
| 45 | log.Print(configErr) |
| 46 | shapes = map[string]claimUnitShape{} |
| 47 | } |
| 48 | state, err := openClaimUnitBindings() |
| 49 | if err != nil { |
| 50 | return err |
| 51 | } |
| 52 | admit := func() error { |
| 53 | if !claimNamedUnit4aComplete { |
| 54 | return hostbox.ErrUnsupported |
| 55 | } |
| 56 | return nil |
| 57 | } |
| 58 | b := &claimUnitBindings{state: state, admit: admit} |
| 59 | b.canonical = func(ctx context.Context, host string) (claimUnitShape, error) { |
| 60 | current, err := productionClaimUnitShapes() |
| 61 | if err != nil { |
| 62 | return claimUnitShape{}, err |
| 63 | } |
| 64 | shape, ok := current[host] |
| 65 | if !ok { |
| 66 | return claimUnitShape{}, os.ErrNotExist |
| 67 | } |
| 68 | return shape, nil |
| 69 | } |
| 70 | b.installation = func(ctx context.Context, host string) (hostguard.RootInstallation, error) { |
| 71 | captured, err := guard.Capture(host) |
| 72 | if err != nil { |
| 73 | return hostguard.RootInstallation{}, err |
| 74 | } |
| 75 | in, err := guard.Root.Installation(ctx, host, captured) |
| 76 | if err != nil { |
| 77 | return in, err |
| 78 | } |
| 79 | observed, err := guard.Claims.Read(ctx, host) |
| 80 | if err != nil { |
| 81 | return in, err |
| 82 | } |
| 83 | if observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil { |
| 84 | return in, os.ErrPermission |
| 85 | } |
| 86 | holder := observed.Claim.Holder |
| 87 | if holder.Incarnation != in.Incarnation || holder.Stamp != in.Stamp || holder.Account != in.Account || holder.App != in.App || holder.Key != in.Record { |
| 88 | return in, os.ErrPermission |
| 89 | } |
| 90 | return in, nil |
| 91 | } |
| 92 | materialState, err := openClaimUnitMaterializations() |
| 93 | if err != nil { |
| 94 | state.Close() |
| 95 | return err |
| 96 | } |
| 97 | files := claimUnitFiles{dir: "/etc/systemd/system"} |
| 98 | material := claimUnitMaterializer{admit: admit, idle: func(host string) error { |
| 99 | binding, err := b.read(host) |
| 100 | if err != nil { |
| 101 | return err |
| 102 | } |
| 103 | unit := base |
| 104 | unit.BoundHost, unit.BoundUnit = host, binding.Shape.Unit |
| 105 | return unit.Gone(ctx, host, "") |
| 106 | }, state: materialState, read: files.read, create: files.create, replace: files.replace} |
| 107 | predecessor := claimPredecessorKernel{ctl: base.Systemctl, group: claimPredecessorGroup, boot: claimUnitBoot} |
| 108 | material.predecessor, material.stopPredecessor = predecessor.capture, predecessor.stop |
| 109 | material.socketCapture, material.socketPark = captureProductionClaimUnitSocket, parkProductionClaimUnitSocket |
| 110 | b.handover = material.handover |
| 111 | dispatch := claimUnitDispatch{base: base, bindings: b, shapes: productionClaimUnitShapes, effective: func(ctx context.Context, shape claimUnitShape) error { |
| 112 | return claimEffectiveUnit(ctx, shape, base.Systemctl) |
| 113 | }, definition: func(shape claimUnitShape) error { |
| 114 | binding, err := b.read(shape.Host) |
| 115 | if err != nil { |
| 116 | return err |
| 117 | } |
| 118 | if binding.Shape != shape { |
| 119 | return os.ErrPermission |
| 120 | } |
| 121 | return material.verify(binding) |
| 122 | }} |
| 123 | h.Launch.Units, h.Launch.unitBindings = dispatch, b |
| 124 | h.Launch.unitProgram = readClaimUnitProgram |
| 125 | h.Launch.boundReady = func(ctx context.Context, host string, started instancectl.ClaimUnitStarted) error { |
| 126 | binding, err := b.read(host) |
| 127 | if errors.Is(err, os.ErrNotExist) { |
| 128 | return nil |
| 129 | } |
| 130 | if err != nil { |
| 131 | return err |
| 132 | } |
| 133 | if binding.Shape.Unit != started.Unit { |
| 134 | return os.ErrPermission |
| 135 | } |
| 136 | binding, err = b.check(ctx, host, nil) |
| 137 | if err != nil { |
| 138 | return err |
| 139 | } |
| 140 | return claimBoundUnitReady(ctx, started, binding.Shape) |
| 141 | } |
| 142 | h.Root.retireBoundUnit = func(ctx context.Context, target hostguard.FenceTarget) error { |
| 143 | if err := retireClaimBoundUnit(ctx, target, b, material, dispatch.Gone); err != nil { |
| 144 | return err |
| 145 | } |
| 146 | _, err := base.Systemctl(ctx, "daemon-reload") |
| 147 | return err |
| 148 | } |
| 149 | kernel, ok := h.Root.Stop.Coordinator.Kernel.(claimUnitStopKernel) |
| 150 | if !ok { |
| 151 | state.Close() |
| 152 | materialState.Close() |
| 153 | return os.ErrPermission |
| 154 | } |
| 155 | kernel.Units = dispatch |
| 156 | h.Root.Stop.Coordinator.Kernel = kernel |
| 157 | if h.Root.executionRuntime != nil { |
| 158 | h.Root.executionRuntime.root = &h.Root |
| 159 | } |
| 160 | closePrevious := a.Close |
| 161 | a.Close = func(ctx context.Context) error { |
| 162 | var err error |
| 163 | if closePrevious != nil { |
| 164 | err = closePrevious(ctx) |
| 165 | } |
| 166 | return errors.Join(err, state.Close(), materialState.Close()) |
| 167 | } |
| 168 | if h.Root.Reconcile.Retirement.JournalDir != "" { |
| 169 | journals, e := hostbox.SavedRetirementJournals(h.Root.Reconcile.Retirement.JournalDir) |
| 170 | if e != nil && !errors.Is(e, os.ErrNotExist) { |
| 171 | log.Print(e) |
| 172 | } |
| 173 | for _, journal := range journals { |
| 174 | if journal.Target != nil { |
| 175 | if e := h.Root.retire(ctx, *journal.Target); e != nil { |
| 176 | log.Print(e) |
| 177 | } |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | hosts := make([]string, 0, len(shapes)) |
| 182 | for host := range shapes { |
| 183 | hosts = append(hosts, host) |
| 184 | } |
| 185 | sort.Strings(hosts) |
| 186 | for _, host := range hosts { |
| 187 | shape := shapes[host] |
| 188 | // Named services must keep their operator definition until a reserved |
| 189 | // boot/adoption controller can keep them running independently of edge. |
| 190 | if !claimNamedUnit4aComplete || shape.Backing == "" { |
| 191 | log.Print(hostbox.ErrUnsupported) |
| 192 | continue |
| 193 | } |
| 194 | if !claimHexDigest(shape.Program) { |
| 195 | log.Print(os.ErrPermission) |
| 196 | continue |
| 197 | } |
| 198 | if _, e := readClaimUnitProgram(shape); e != nil { |
| 199 | log.Print(e) |
| 200 | continue |
| 201 | } |
| 202 | next, e := b.current(ctx, host) |
| 203 | if e != nil { |
| 204 | log.Print(e) |
| 205 | continue |
| 206 | } |
| 207 | if e = guard.WithLocal(ctx, host, func() error { |
| 208 | current, e := guard.Root.Installation(ctx, host, hostguard.Captured{Incarnation: next.Owner.Incarnation, Stamp: next.Owner.Stamp}) |
| 209 | if e != nil { |
| 210 | return e |
| 211 | } |
| 212 | if unitBindingOwner(current) != next.Owner { |
| 213 | return os.ErrPermission |
| 214 | } |
| 215 | binding, e := b.establish(ctx, host, next) |
| 216 | if e != nil { |
| 217 | return e |
| 218 | } |
| 219 | if e = material.install(ctx, binding); e != nil { |
| 220 | return e |
| 221 | } |
| 222 | if _, e = base.Systemctl(ctx, "daemon-reload"); e != nil { |
| 223 | return e |
| 224 | } |
| 225 | if e = claimEffectiveUnit(ctx, binding.Shape, base.Systemctl); e != nil { |
| 226 | return e |
| 227 | } |
| 228 | return projectClaimBoundSocket(binding.Shape) |
| 229 | }); e != nil { |
| 230 | log.Print(e) |
| 231 | } |
| 232 | } |
| 233 | if len(hosts) != 0 { |
| 234 | if _, e := base.Systemctl(ctx, "daemon-reload"); e != nil { |
| 235 | log.Print(e) |
| 236 | } |
| 237 | } |
| 238 | return nil |
| 239 | } |
| 240 | |