package host import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rollout" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" "github.com/carlosframework/platform/internal/safefile" ) type rootUpdateRecord struct { RetryMirror *rootLegacyRetry `json:",omitempty"` Schema int Installation string Route registry.Route Build release.BuildStateCopy Consumed *release.Consumed Served map[string]rootServedBuild } type rootServedBuild struct { Digest, Invocation, Source string Epoch uint64 } type rootUpdateSource struct { enforceFailures bool heldGeneration *release.GenerationBinding legacy []byte ready func(registry.Route) error publish func(string, rootRetryStatusRecord) error source rootRestartRequestSource binding *proofBindingSource state rootstate.Store book *rollout.RootFailures store release.Store pub ed25519.PublicKey observe func(registry.Route) (liveBuild, error) data string } func rootUpdatesMigration(c rootparticipation.Census) string { return fmt.Sprintf("%s/%s/%d", c.Box, c.Enrollment, c.Migration) } func rootFailureLegacy(data string) ([]byte, error) { f, err := safefile.OpenFile(filepath.Join(data, rollout.FailureFile), os.O_RDONLY, 0) if errors.Is(err, os.ErrNotExist) { return nil, nil } if err != nil { return nil, err } defer f.Close() b, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1)) if len(b) > rootstate.MaxRecord { return nil, os.ErrPermission } return b, err } func productionRootUpdates(dd string, probes ...func() (rootparticipation.Ledger, bool, error)) release.RootUpdateOpener { return productionRootUpdatesHeld(dd, nil, probes...) } func productionRootUpdatesHeld(dd string, held *release.GenerationBinding, probes ...func() (rootparticipation.Ledger, bool, error)) release.RootUpdateOpener { return func(ctx context.Context, store release.Store, pub ed25519.PublicKey) (*release.RootUpdateInputs, error) { probe := rootparticipation.OpenForUpdate if len(probes) > 1 || len(probes) == 1 && probes[0] == nil { return nil, os.ErrPermission } if len(probes) == 1 { probe = probes[0] } ledger, required, err := probe() if ledger.State != nil { defer ledger.State.Close() } if err != nil { return nil, err } if !required { return nil, nil } c, err := ledger.Census() if err != nil { return nil, err } if !c.Complete || c.NeverEnrolled { return nil, os.ErrPermission } state, err := rootstate.Open(rollout.RootFailuresDir, false, false) if err != nil { return nil, err } fail := func(err error) (*release.RootUpdateInputs, error) { state.Close(); return nil, err } book, err := rollout.OpenRootFailures(state, rootUpdatesMigration(c)) if err != nil { return fail(err) } legacy, _ := rootFailureLegacy(dd) fences, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false) if err != nil { return fail(err) } binding := productionProofBindingSource(store, systemdUnits{}, release.OptDir()) binding.cacheIndex = false binding.claim = func(ctx context.Context, host string) (hostclaim.Observed, error) { return hostclaim.Read(ctx, store, host) } binding.box = func(context.Context) (string, error) { return c.Box, nil } observer := productionProofObserver(release.OptDir()) observer.resolve = binding.resolve p := rootUpdateSource{legacy: legacy, ready: func(rt registry.Route) error { return rootUpdateReady(ctx, rt) }, publish: publishRootRetry, source: rootRestartRequestSource{state: fences, store: store, census: func() (rootparticipation.Census, error) { return c, nil }}, binding: binding, state: state, book: book, store: store, pub: pub, data: dd, observe: func(rt registry.Route) (liveBuild, error) { return observer.observe(rt, 0) }} p.heldGeneration = held return &release.RootUpdateInputs{Resolve: p.resolve, Close: func() error { return errors.Join(fences.Close(), state.Close()) }}, nil } } func (p rootUpdateSource) resolve(ctx context.Context, host string) (release.RootUpdateInput, error) { var out release.RootUpdateInput var in hostguard.RootInstallation var err error if p.heldGeneration != nil { in, err = p.source.launchInstallation(ctx, host, *p.heldGeneration) } else { in, err = p.source.installation(ctx, host) } if err != nil { return out, err } if in.Empty || in.Record != provision.RecordKey(in.Account, in.App, host) { return out, os.ErrPermission } rec, etag, err := provision.GetRecordETag(ctx, p.store, in.Account, in.App, host) if err != nil { return out, err } inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt) if err != nil || inc != in.Incarnation || etag == "" { return out, os.ErrPermission } if _, err = p.binding.claimIdentity(ctx, rec); err != nil { return out, err } var policy *provision.UpdatePolicy manifest, err := provision.GetManifest(ctx, p.store, rec.Account, rec.App) if err == nil { policy = manifest.Updates } else if !errors.Is(err, os.ErrNotExist) { return out, err } if err = provision.ValidatePolicy(policy); err != nil { return out, err } rt := provision.RouteFor(*rec, p.data, "", true, policy) // A remote proposal cannot supply protected local lifecycle or served facts. rt.GoodVersion, rt.GoodDigest, rt.GoodDemand, rt.ServingVersion, rt.BuildState = "", "", "", "", "" installation := rootHistoryInstallation(in) key := rootSettingsKey(host, installation) var saved rootUpdateRecord raw, err := p.state.Read(key) if errors.Is(err, os.ErrNotExist) { old, readErr := p.state.Read("settings-" + host + ".json") if readErr == nil { var previous rootUpdateRecord if rootparticipation.Decode(old, &previous) != nil || previous.Schema != 1 { return out, os.ErrPermission } if previous.Installation == installation { raw, err = old, nil } } else if !errors.Is(readErr, os.ErrNotExist) { return out, readErr } } if err == nil { if rootparticipation.Decode(raw, &saved) != nil || saved.Schema != 1 || saved.Installation != installation || saved.Route.Host != host || saved.Route.Account != rec.Account || saved.Route.App != rec.App { return out, os.ErrPermission } } else if errors.Is(err, os.ErrNotExist) { saved = rootUpdateRecord{Schema: 1, Installation: installation, Route: rt, Served: map[string]rootServedBuild{}} } else { return out, err } save := func() error { body, err := json.Marshal(saved) if err != nil { return err } return p.state.Write(key, body) } // Observe canonical process, executable digest and readiness together. Registry // good-build fields and remote build-state never enter this record. if rt.Unit != "" && p.observe != nil { live, e := p.observe(rt) if e == nil && live.binding.authorizedRecovery() && live.binding.RecordKey == in.Record && live.binding.RecordETag == etag && live.binding.Digest == live.SHA256 && live.binding.Version == live.Version && live.binding.Route.Host == host && live.binding.Route.Account == rec.Account && live.binding.Route.App == rec.App && live.Version != "" && live.SHA256 != "" && live.invocation != "" && live.binding.Source != "" { if saved.Served == nil { saved.Served = map[string]rootServedBuild{} } epoch := p.book.RestrictionEpoch(rec.Account, rec.App, live.Version, host) saved.Served[live.Version] = rootServedBuild{live.SHA256, live.invocation, live.binding.Source, epoch} saved.Build = release.BuildStateCopy{Known: true, State: &release.BuildState{Schema: 1, Host: host, LastExecuted: live.Version}} if saved.Consumed != nil && saved.Consumed.Resolved != nil && saved.Consumed.Resolved.Target == live.Version { saved.Build.State.Resolved = saved.Consumed.Resolved saved.Build.State.Completed = saved.Consumed.Resolved.ID } } } saved.Route = rt if err = save(); err != nil { return out, err } retries, _, err := release.LoadHostRetries(ctx, p.store, rec.Account, rec.App) if err != nil { return out, err } for _, r := range retries.Requests { if r.Scope.Kind == release.RestartScopeRetry && r.Scope.Value == host { if grant, e := release.VerifyHostRetry(p.pub, r.Scope.Signed, rec.Account, rec.App, host, time.Now()); e == nil { exact := p.book.ID() == grant.Book && p.book.PublishedEpoch(rec.Account, rec.App, grant.Build, host) == grant.Epoch if exact { legacyBook, epoch := rollout.LegacyRetryIdentity(p.legacy, rec.Account, rec.App) if legacyBook != "" && epoch != 0 { saved.RetryMirror = &rootLegacyRetry{Build: grant.Build, Book: legacyBook, Epoch: epoch} } if err = save(); err != nil { return out, err } } if err = p.book.VerifiedRetry(p.pub, r.Scope.Signed, rec.Account, rec.App, host, time.Now()); err != nil { return out, err } } } } ptr, e := release.Channel(ctx, p.store, p.pub, rec.Account, rec.App, rec.Channel) if e == nil { if served, ok := saved.Served[ptr.Version]; ok && served.Digest != "" { if m, e := release.AuthorizedManifest(ctx, p.store, p.pub, rec.Account, rec.App, ptr.Version); e == nil && m.Artifacts[release.AdoptPlatform()].SHA256 == served.Digest { if _, err = p.book.ClearHoldRanOnPointer(host, rec.Account, rec.App, ptr.PromotedAt); err != nil { return out, err } } } if _, err = p.book.ClearOnPointer(rec.Account, rec.App, ptr.Version, ptr.PromotedAt); err != nil { return out, err } } out = release.RootUpdateInput{Route: rt, Build: saved.Build, Consumed: saved.Consumed, Failed: func(build string) bool { if !p.enforceFailures { return false } served, ok := saved.Served[build] ok = ok && served.Digest != "" && served.Invocation != "" && served.Source != "" && served.Epoch == p.book.RestrictionEpoch(rec.Account, rec.App, build, host) if ok { manifest, err := release.AuthorizedManifest(ctx, p.store, p.pub, rec.Account, rec.App, build) ok = err == nil && manifest.Artifacts[release.AdoptPlatform()].SHA256 == served.Digest } if mark, held := p.book.HoldRanOn(host); held && mark.Account == rec.Account && mark.App == rec.App && mark.Build != build && saved.Build.State != nil && saved.Build.State.LastExecuted == build { return true } return p.book.SaveOwed() || p.book.Blocks(rec.Account, rec.App, build, host, ok) || p.book.HeldDown(rec.Account, rec.App, build, host) }, Save: func(c release.Consumed) error { saved.Consumed = &c; return save() }} out.Begin = func(version, digest, attempt string) (func(string, error, func() error) error, error) { return p.beginRuntime(&saved, save, version, digest, attempt) } projectedVersion := rec.Pin if projectedVersion == "" && ptr != nil { projectedVersion = ptr.Version } if err = p.publishRetry(saved, projectedVersion); err != nil && p.enforceFailures { return release.RootUpdateInput{}, err } if saved.Build.State != nil { out.Installed = saved.Build.State.LastExecuted } return out, nil } func initializeMigrationFailures(dd string, execution rootstate.Store) error { c, err := (rootparticipation.Ledger{State: execution}).Census() if err != nil { return err } if !c.Complete || c.NeverEnrolled { return os.ErrPermission } s, err := rootstate.Open(rollout.RootFailuresDir, true, false) if err != nil { return err } defer s.Close() raw, err := rootFailureLegacy(dd) if err != nil { return err } _, err = rollout.InitializeRootFailures(s, rootUpdatesMigration(c), raw) return err } func rootSettingsKey(host, installation string) string { sum := sha256.Sum256([]byte(host + "\x00" + installation)) return "settings-" + hex.EncodeToString(sum[:]) + ".json" }