| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "flag" |
| 7 | "log" |
| 8 | "os" |
| 9 | "os/signal" |
| 10 | "path/filepath" |
| 11 | "syscall" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/hostbox" |
| 14 | "github.com/carlosframework/platform/internal/hostwrite" |
| 15 | "github.com/carlosframework/platform/internal/release" |
| 16 | ) |
| 17 | |
| 18 | // checkClaimJournalMigration is root startup's only contact with the legacy |
| 19 | // claim journals under the agent's data directory. It never imports and never |
| 20 | // quiesces: legacy files the agent changes after the marker are reported and |
| 21 | // ignored, so they cannot stop root's control plane. |
| 22 | func checkClaimJournalMigration(ctx context.Context, journals *hostbox.JournalService, local string) error { |
| 23 | alert, err := journals.CheckLegacyMigration(ctx, local) |
| 24 | if alert != nil { |
| 25 | log.Printf("claim journal migration: %v", alert) |
| 26 | } |
| 27 | return err |
| 28 | } |
| 29 | |
| 30 | // RunSystemJournalMigrate is the offline operator step that imports legacy |
| 31 | // claim journals into root's journal: carlos system journal-migrate, run as |
| 32 | // root with the edge and the agent stopped. SIGTERM and interrupt cancel the |
| 33 | // import, so the edge's runtime mask is lifted on the way out. |
| 34 | func RunSystemJournalMigrate(args []string) { |
| 35 | fs := flag.NewFlagSet("system journal-migrate", flag.ExitOnError) |
| 36 | window := fs.String("window", "", journalMigrateWindowFlag) |
| 37 | _ = fs.Parse(args) |
| 38 | if os.Geteuid() != 0 || fs.NArg() != 0 { |
| 39 | log.Fatal(hostwrite.ErrUnavailable) |
| 40 | } |
| 41 | ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt) |
| 42 | defer stop() |
| 43 | ctx, err := claimEntryContext(ctx, os.Getenv, openDeployStore) |
| 44 | if err != nil { |
| 45 | log.Fatal(err) |
| 46 | } |
| 47 | if !hostwrite.Enabled(claimEntryEnv(ctx)) { |
| 48 | log.Fatal(hostwrite.ErrUnavailable) |
| 49 | } |
| 50 | reg, err := openServiceRegistry(filepath.Join(dataDir(), "registry.db")) |
| 51 | if err != nil { |
| 52 | log.Fatal(err) |
| 53 | } |
| 54 | defer reg.Close() |
| 55 | provider := productionClaimOwners() |
| 56 | if provider == nil { |
| 57 | provider = ProductionClaimOwnerProvider{} |
| 58 | } |
| 59 | r := ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: dataDir(), OptDir: release.OptDir(), UnitsDir: "/etc/systemd/system", MigrationWindow: *window} |
| 60 | // The window is checked before anything is written; the census checks |
| 61 | // its masks again when it walks the unit directory. |
| 62 | if _, err = verifyJournalMigrateWindow(r); err != nil { |
| 63 | log.Fatal(err) |
| 64 | } |
| 65 | if err = migrateClaimJournalsWith(ctx, claimStore(ctx), r, provider, true); err != nil { |
| 66 | log.Fatal(err) |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | // journalMigrateWindowFlag is --window's help. |
| 71 | const journalMigrateWindowFlag = "the attended window's directory: its held directory keeps the original of each window unit the window masked, so the migration census accepts those masks" |
| 72 | |
| 73 | func migrateClaimJournalsWith(ctx context.Context, store release.Store, r ClaimRuntimeResources, provider ClaimOwnerProvider, census bool) (resultErr error) { |
| 74 | if store == nil || provider == nil || r.Mode != "instance-control" || !filepath.IsAbs(r.DataDir) { |
| 75 | return hostwrite.ErrUnavailable |
| 76 | } |
| 77 | if census { |
| 78 | if err := claimActivationPreflight(r.Registry); err != nil { |
| 79 | return err |
| 80 | } |
| 81 | } |
| 82 | if census { |
| 83 | if err := claimActivationVerified(ctx, store, r); err != nil { |
| 84 | return err |
| 85 | } |
| 86 | } |
| 87 | owners, err := provider.OpenClaimOwners(ctx, store, r) |
| 88 | if err != nil { |
| 89 | return err |
| 90 | } |
| 91 | if owners.Close == nil { |
| 92 | return hostwrite.ErrUnavailable |
| 93 | } |
| 94 | defer func() { resultErr = errors.Join(resultErr, owners.Close(context.WithoutCancel(ctx))) }() |
| 95 | if owners.RetirementAuthority == nil { |
| 96 | return hostwrite.ErrUnavailable |
| 97 | } |
| 98 | actor, err := owners.actorForConstruction(ctx, r.Mode) |
| 99 | if err != nil { |
| 100 | return err |
| 101 | } |
| 102 | launches, err := openClaimRootJournal() |
| 103 | if err != nil { |
| 104 | return err |
| 105 | } |
| 106 | defer func() { resultErr = errors.Join(resultErr, launches.Close()) }() |
| 107 | journals, err := hostbox.NewJournalService(launches) |
| 108 | if err != nil { |
| 109 | return err |
| 110 | } |
| 111 | if census { |
| 112 | return migrateRootClaimAuthority(ctx, store, r, owners, actor, journals, launches) |
| 113 | } |
| 114 | return journals.MigrateLegacy(ctx, filepath.Join(r.DataDir, ".hostclaims"), actor.Box, actor.Enrollment, quiesceLegacyClaimJournals, legacyJournalProof(store, owners.RetirementAuthority.Directory), recoverLegacyClaimEdgeMask) |
| 115 | } |
| 116 | |
| 117 | var openClaimRootJournal = func() (*hostbox.LaunchJournal, error) { |
| 118 | if err := hostbox.PrepareRootJournalDirectory("/var/lib/carlos-claims"); err != nil { |
| 119 | return nil, err |
| 120 | } |
| 121 | return hostbox.OpenLaunchJournal("/var/lib/carlos-claims/journals.db") |
| 122 | } |
| 123 | |