carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3// The moves wake source (moves Plan 5 Task 9b; the rulings after claim Plan
4// 12: Q1, Q12, Q15, and the controller's Task 8 overrides). It is this box's
5// ClaimProductionMoveSources: registered with the claim at agent start,
6// right after the park providers (registerProductionMovesParks) and before
7// newClaimWiring, and only with both CARLOS_HOST_CLAIM and
8// CARLOS_MOVES_PHASE1 on. Both default to off, so nothing here turns
9// anything on.
10//
11// Registering it (registerMovesWakeSource) is what opens the one gate
12// between the moves parks and production (movesWakeSourceRegistered), so
13// the parks arm in the same agent start as the wake owners, never before.
14// With the gate open, productionClaimRegional.View, the claim's park view
15// for every host on the box, reads through the moves view: the claim's
16// at-rest sweep and every regional park chore run through it for every
17// host, not only moved ones (Q1's widened sweep; the rehearsal's evidence
18// list names it).
19//
20// - Open builds the moves owner set the claim binds under moves C1
21// (claimMovesC1): one movesProbeOwners, which serves the landing probe,
22// the committed landing wake and, dispatched by kind, the recovery
23// owners (restoring and reverting), with the moves attempt journal as
24// Operations, the moves loss owner as LossRecovery and the loss
25// baseline validator as LossSources. Its route, launch and held works
26// are the claim's move read access; its held release is Task 7b's root
27// adapter (heldReleaseProbeRelease). Open binds no files manifest
28// provider: the recovery owners take the claim's C8 provider, the one
29// Task 6's RegisterProductionClaimFiles binds for the serving owners
30// (openClaimFilesManifest, bindClaimMovesFiles). Two providers are a
31// wiring error.
32// - View and VerifyRetirement are never read by the claim (Q15): the park
33// sources registered first stay, so both delegate to those same values.
34//
35// Production arming still needs, beyond this registration:
36//
37// - both switches on (CARLOS_HOST_CLAIM, CARLOS_MOVES_PHASE1);
38// - the claim floor (hostfloor: the guard-ready running floor);
39// - the switch default: moves.DefaultEnabled stays false while copy rows
40// are pending (TestPendingCopyKeepsTheSwitchOff);
41// - on a home box, claim Plan 13's signed fleet home designation, so the
42// claim grants the @home alias (armMovesParks refuses otherwise);
43// - claim activation's #250 checklist: the files manifest backfill for
44// hosts that predate the files publisher, CARLOS_RELEASE_PUBKEY for
45// instance-control's held release route, claim Plan 14's root
46// authority, and claim Plan 15's provenance record for hosts past the
47// admission pruning window.
48
49import (
50 "context"
51 "crypto/ecdh"
52 "crypto/rand"
53 "encoding/hex"
54 "errors"
55 "fmt"
56 "strings"
57 "time"
58
59 "github.com/carlosframework/platform/internal/appconfig"
60 "github.com/carlosframework/platform/internal/controlsock"
61 "github.com/carlosframework/platform/internal/hostbox"
62 "github.com/carlosframework/platform/internal/hostclaim"
63 "github.com/carlosframework/platform/internal/hostwrite"
64 "github.com/carlosframework/platform/internal/lease"
65 "github.com/carlosframework/platform/internal/moves"
66 "github.com/carlosframework/platform/internal/park"
67 "github.com/carlosframework/platform/internal/provision"
68 "github.com/carlosframework/platform/internal/registry"
69 "github.com/carlosframework/platform/internal/release"
70)
71
72// movesWakeSources is this box's moves wake source (see the file comment).
73type movesWakeSources struct {
74 // Parks are the park sources registered before it: View and
75 // VerifyRetirement delegate to them (Q15).
76 Parks movesParkSources
77 // DataDir is the moves data directory: the op journals' root and the
78 // hosts' files directories, the moves worker's (movesDataDir).
79 DataDir string
80 // OptDir is /opt/carlos, where root publishes the held release.
81 OptDir string
82 // Region is this box's CARLOS_REGION, the held release request's region.
83 Region string
84 // Getenv reads CARLOS_SECRETS_KEY when the source opens.
85 Getenv func(string) string
86
87 // Seams, set by newProductionMovesWakeSources.
88 records func(release.Store) MovesRecordReader
89 markers func(ctx context.Context, bucket, region string) (ClaimMarkerStore, error)
90 relay heldReleaseRelay
91 now func() time.Time
92 newID func() string
93}
94
95var _ ClaimProductionMoveSources = (*movesWakeSources)(nil)
96
97// newProductionMovesWakeSources is the production source over parks and
98// the box's data directory.
99func newProductionMovesWakeSources(getenv func(string) string, parks movesParkSources, dataDir string) *movesWakeSources {
100 return &movesWakeSources{Parks: parks, DataDir: movesDataDir(dataDir), OptDir: release.OptDir(), Region: strings.TrimSpace(getenv("CARLOS_REGION")), Getenv: getenv,
101 records: MovesRecordsFrom, markers: newProductionClaimMarkers, relay: newHeldReleaseRootClient(controlsock.Path), now: time.Now, newID: movesWakeID}
102}
103
104// movesWakeID is a fresh generation name, as the claim's own.
105func movesWakeID() string {
106 var b [16]byte
107 if _, err := rand.Read(b[:]); err != nil {
108 return ""
109 }
110 return hex.EncodeToString(b[:])
111}
112
113// Open builds the moves owner set for the verified actor, over the claim's
114// store, its versioned data bucket and its move read access (see the file
115// comment). Anything missing refuses: the claim then refuses every wake by
116// name rather than half-wiring moves.
117func (s *movesWakeSources) Open(ctx context.Context, store release.Store, data lease.VersionedStore, actor hostbox.Actor, read ClaimMoveReadAccess) (ClaimMovesOwners, ClaimFilesManifest, error) {
118 region := park.PlacementRegion(actor.Region)
119 if s == nil || ctx == nil || store == nil || data == nil || read == nil || s.Getenv == nil || s.records == nil || s.markers == nil || s.relay == nil || s.now == nil || s.newID == nil ||
120 s.DataDir == "" || s.OptDir == "" || actor.Box == "" || actor.Bucket == "" || region == "" || actor.Cloud.Region == "" {
121 return ClaimMovesOwners{}, nil, hostwrite.ErrUnavailable
122 }
123 if a, ok := read.(*claimMoveReadAccess); ok && a == nil {
124 return ClaimMovesOwners{}, nil, hostwrite.ErrUnavailable
125 }
126 secrets := s.Getenv("CARLOS_SECRETS_KEY")
127 envKey, err := moves.ReportKeyFromSecrets(secrets)
128 if err != nil {
129 return ClaimMovesOwners{}, nil, err
130 }
131 priv, err := appconfig.ParsePrivateKey(secrets, "CARLOS_SECRETS_KEY")
132 if err != nil {
133 return ClaimMovesOwners{}, nil, err
134 }
135 markers, err := s.markers(ctx, actor.Bucket, actor.Cloud.Region)
136 if err != nil {
137 return ClaimMovesOwners{}, nil, err
138 }
139 records := s.records(store)
140 via := heldReleaseVia{Relay: s.relay, Region: s.Region}
141 owners := &movesProbeOwners{HomeRegion: actor.HomeRegion, FleetHomeRegion: actor.FleetHomeRegion, Claims: store, Records: records,
142 ReadyPath: movesReadyPath(store), Data: data, Bucket: actor.Bucket, Region: region, Box: actor.Box,
143 DataDir: s.DataDir, FilesDir: movesFilesDirFor(s.DataDir), Markers: markers,
144 Route: movesReadRoute(read),
145 Launch: movesReadLaunch(read),
146 EnvKey: envKey, Admitted: MovesRecordAdmittedAttempts,
147 Works: read.HeldWork,
148 Render: movesProbeRender(store, priv, s.DataDir, actor.Box),
149 Release: heldReleaseProbeRelease(via, store, actor.Box, s.OptDir, s.DataDir, release.NewRootProofGate(release.ProofPolicyPath, s.DataDir)),
150 NewID: s.newID, Now: s.now, Page: PageOperator}
151 if !owners.valid() {
152 return ClaimMovesOwners{}, nil, hostwrite.ErrUnavailable
153 }
154 attempts := &movesProbeAttempts{Claims: store, Records: records, DataDir: s.DataDir, Admitted: MovesRecordAdmittedAttempts, NewID: s.newID, Now: s.now,
155 RecoveryAttempt: movesWorkerRecoveryAttempt(store)}
156 return owners.MovesOwners(attempts), nil, nil
157}
158
159// View is the park sources' view (Q15): the claim reads the parks
160// registered first, so this is the same view.
161func (s *movesWakeSources) View(ctx context.Context, o hostclaim.Observed) (park.View, error) {
162 if s == nil || s.Parks.View == nil {
163 return park.View{}, hostwrite.ErrUnavailable
164 }
165 return s.Parks.View(ctx, o)
166}
167
168// VerifyRetirement is the park sources' evidence (Q15).
169func (s *movesWakeSources) VerifyRetirement(ctx context.Context, o hostclaim.Observed, v []lease.ObjectVersion) (string, time.Time, error) {
170 if s == nil || s.Parks.Evidence == nil {
171 return "", time.Time{}, hostwrite.ErrUnavailable
172 }
173 return s.Parks.Evidence.VerifyRetirement(ctx, o, v)
174}
175
176// Close closes nothing: Open holds no resource of its own.
177func (s *movesWakeSources) Close(context.Context) error { return nil }
178
179// movesReadyPath is the app manifest's moves.ready_path, "/" when the
180// manifest declares none; a manifest that cannot be read refuses (S1's rule,
181// probeSource).
182func movesReadyPath(store release.Store) func(ctx context.Context, account, app string) (string, error) {
183 return func(ctx context.Context, account, app string) (string, error) {
184 m, err := provision.GetManifest(ctx, store, account, app)
185 if err != nil {
186 return "", err
187 }
188 return m.ReadyPathOrDefault(), nil
189 }
190}
191
192// movesReadRoute is the claim's move read access as the owners' route:
193// each read is bounded by the claim's request limit.
194func movesReadRoute(read ClaimMoveReadAccess) func(host string) (*registry.Route, error) {
195 return func(host string) (*registry.Route, error) {
196 ctx, cancel := context.WithTimeout(context.Background(), hostbox.RequestLimit)
197 defer cancel()
198 return read.Route(ctx, host)
199 }
200}
201
202// movesReadLaunch is the claim's move read access as the owners' launch,
203// with the access's env report: made from the same composition as the
204// launch's env (claimMoveLaunchSource), its digest is the launch's
205// move-portable pin, which the owners check (fix round 4).
206func movesReadLaunch(read ClaimMoveReadAccess) func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
207 return func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
208 return read.Launch(ctx, rt.Host)
209 }
210}
211
212// movesProbeRender is the held render under the claim for the probe and
213// recovery owners (appconfig.RenderHeldUnder, the held render authority).
214func movesProbeRender(store release.Store, priv *ecdh.PrivateKey, dataDir, box string) func(ctx context.Context, rt registry.Route, op string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) {
215 return func(ctx context.Context, rt registry.Route, op string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) {
216 return appconfig.RenderHeldUnder(ctx, store, priv, dataDir, rt, op, box, work)
217 }
218}
219
220// movesWorkerRecoveryAttempt is the attempt journal's RecoveryAttempt: the
221// armed moves worker's move log hook (recoveryAttemptLogger). The worker is
222// armed after the claim wiring opens this source, so it is looked up when a
223// start is counted; with none armed nothing is logged.
224func movesWorkerRecoveryAttempt(store release.Store) func(ctx context.Context, rec provision.Record, epoch uint64, n int) {
225 return func(ctx context.Context, rec provision.Record, epoch uint64, n int) {
226 if w := armedMovesWorker.Load(); w != nil {
227 w.recoveryAttemptLogger(store)(ctx, rec, epoch, n)
228 }
229 }
230}
231
232// registerProductionMovesWakeSource is the agent's registration, right
233// after the parks (registerProductionMovesParks) and before
234// newClaimWiring: only with both CARLOS_HOST_CLAIM and CARLOS_MOVES_PHASE1
235// on, over the park sources just registered. Off, it registers nothing and
236// the gate stays shut. A registration that succeeds opens the gate.
237func registerProductionMovesWakeSource(getenv func(string) string, parks movesParkSources, dataDir string) error {
238 if getenv == nil || !hostwrite.Enabled(getenv) || !moves.Enabled(getenv) {
239 return nil
240 }
241 if parks.View == nil || parks.Evidence == nil || dataDir == "" {
242 return errors.Join(hostwrite.ErrUnavailable, errors.New("the moves wake source needs the registered park sources and the data directory"))
243 }
244 return registerMovesWakeSource(newProductionMovesWakeSources(getenv, parks, dataDir))
245}
246
247// claimMovesFilesBinder is the moves owner set's C8 binding: the claim's
248// files manifest provider, the one the serving owners use.
249type claimMovesFilesBinder interface {
250 bindClaimFiles(ClaimFilesManifest) error
251}
252
253// bindClaimMovesFiles hands the claim's C8 provider (out.Serving.Files,
254// bound by openClaimFilesManifest from Task 6's registration) to the moves
255// owners, so the recovery owners establish through the same provider. A
256// moves owner set already bound to another provider is a wiring error.
257func bindClaimMovesFiles(out *ClaimOwnerBindings) error {
258 if out == nil || out.Serving.Files == nil {
259 return nil
260 }
261 b, ok := out.Moves.Inputs.(claimMovesFilesBinder)
262 if !ok {
263 return nil
264 }
265 return b.bindClaimFiles(out.Serving.Files)
266}
267
268// bindClaimFiles binds the claim's C8 provider once; the same provider
269// again is a no-op, another one is refused.
270func (m *movesProbeOwners) bindClaimFiles(files ClaimFilesManifest) error {
271 if m == nil || files == nil {
272 return hostwrite.ErrUnavailable
273 }
274 m.mu.Lock()
275 defer m.mu.Unlock()
276 if m.Files != nil && m.Files != files {
277 return errors.Join(hostwrite.ErrUnavailable, fmt.Errorf("host claim: two C8 files manifest providers: the moves owners hold %T", m.Files))
278 }
279 m.Files = files
280 return nil
281}
282