//go:build linux package activator import ( "context" "crypto/sha256" "encoding/hex" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/instancekey" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/seeds" ) // importedRoute is nestedBackend's row as an import's swap leaves it: a // fresh seed id, swapped, its snapshot in the box's seed reader. func importedRoute(t *testing.T, b *ExecBackend, rt registry.Route) registry.Route { t.Helper() id, _ := seeds.NewID() snap := filepath.Join(t.TempDir(), "snap.db") if err := writeMinimalSQLite(snap); err != nil { t.Fatal(err) } body, _ := os.ReadFile(snap) sum := sha256.Sum256(body) rt.SeedSource = seeds.RowSeed{ID: id, Kind: seeds.KindImport, SHA256: hex.EncodeToString(sum[:]), Bytes: int64(len(body)), State: seeds.StateSwapped}.Encode() b.Seeds = &fakeSeedObjects{body: map[string][]byte{id: body}} return rt } // launchKey is one launch of rt as doWake makes it (the restore, then the // run role's preparation): the key the app is handed, and the key the // instance's own env file holds ("" for none). func launchKey(t *testing.T, b *ExecBackend, rt registry.Route) (handed, own string) { t.Helper() if fresh, err := b.Restore(context.Background(), rt); err != nil || fresh { t.Fatalf("Restore = fresh %v, %v", fresh, err) } _, ctl, logs, env, err := b.prepare(rt, instancectl.RoleRun) if err != nil { t.Fatalf("prepare: %v", err) } logs.Close() if ctl != nil { ctl.Close() } raw, err := os.ReadFile(filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), rt.Host+".env")) if err != nil && !os.IsNotExist(err) { t.Fatal(err) } return env[InstanceKeyVar], parseEnvBytes(raw)[InstanceKeyVar] } // Merge review S1, the reported sequence: an import is verified (its first // launch mints key A), aborted and rolled back; the owner then configures // the old process's key B and imports the host again. The second import is // handed B. Before the fix it was handed A, which the first import's // rollback left in .env, where it outranks configuration. func TestAnAbortedImportsKeyDoesNotOutliveItsRollback(t *testing.T) { noOwnReplica(t) b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{}) dir := provision.InstanceDir(b.DataDir, rt.Host) // The swap, before the import's row exists: this host has no env file. before, err := SeedEnvBefore(rt.DBPath, rt.Host) if err != nil || before != registry.EnvBeforeAbsent { t.Fatalf("SeedEnvBefore = %q, %v; want absent", before, err) } // Verify: the import's first launch mints key A. first := importedRoute(t, b, rt) keyA, ownA := launchKey(t, b, first) if !wellFormedInstanceKey(keyA) || ownA != keyA { t.Fatal("the import's first launch did not mint a key into its own env file") } // The adoption record a helper would have written for this writer. if err := os.WriteFile(filepath.Join(dir, rt.Host+instancekey.AdoptionSuffix), []byte("{}\n"), 0o600); err != nil { t.Fatal(err) } // Abort: the fenced rollback's step 3, as seedwiring wires it. if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil { t.Fatalf("RetireSeedKey: %v", err) } if err := PurgeSeedLocal(rt.DBPath); err != nil { t.Fatal(err) } for _, n := range []string{rt.Host + ".env", rt.Host + instancekey.AdoptionSuffix} { if _, err := os.Lstat(filepath.Join(dir, n)); !os.IsNotExist(err) { t.Fatalf("%s survived the rollback (%v): it was the import's", n, err) } } if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil { t.Fatalf("a second retirement: %v", err) } // The owner configures the old process's key B; the config sync // delivers it. keyB := strings.Repeat("B", 43) + "=" if err := os.WriteFile(filepath.Join(dir, rt.Host+".config.env"), []byte(InstanceKeyVar+"="+keyB+"\n"), 0o600); err != nil { t.Fatal(err) } // Reimport, same host: its swap finds no env file again, and its first // launch is handed B. Nothing is minted while configuration sets it. if before, err := SeedEnvBefore(rt.DBPath, rt.Host); err != nil || before != registry.EnvBeforeAbsent { t.Fatalf("SeedEnvBefore at the reimport = %q, %v; want absent", before, err) } second := importedRoute(t, b, rt) handed, own := launchKey(t, b, second) if handed != keyB { if handed == keyA { t.Fatal("the reimport was handed the aborted import's key, not the configured one") } t.Fatalf("the reimport was handed a key that is not the configured one") } if own != "" { t.Fatal("a key was minted into the own env file while configuration sets one") } } // Merge review S1: a key, and any env content, that predated the import // survives its rollback. Only what the import created is retired. func TestRetireSeedKeyLeavesWhatPredatesTheImport(t *testing.T) { token := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("e", 64) t.Run("a key that was there before", func(t *testing.T) { noOwnReplica(t) b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{}) dir := provision.InstanceDir(b.DataDir, rt.Host) keyP := strings.Repeat("P", 43) + "=" predating := []byte(token + "\n" + InstanceKeyVar + "=" + keyP + "\n") if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } envPath := filepath.Join(dir, rt.Host+".env") if err := os.WriteFile(envPath, predating, 0o600); err != nil { t.Fatal(err) } adoption := filepath.Join(dir, rt.Host+instancekey.AdoptionSuffix) if err := os.WriteFile(adoption, []byte("{}\n"), 0o600); err != nil { t.Fatal(err) } before, err := SeedEnvBefore(rt.DBPath, rt.Host) if err != nil || before != registry.EnvBeforeKey { t.Fatalf("SeedEnvBefore = %q, %v; want key", before, err) } // The import launches with the key that was there: nothing minted. if handed, _ := launchKey(t, b, importedRoute(t, b, rt)); handed != keyP { t.Fatal("the import was not handed the key that predated it") } if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil { t.Fatal(err) } if err := PurgeSeedLocal(rt.DBPath); err != nil { t.Fatal(err) } if got, err := os.ReadFile(envPath); err != nil || string(got) != string(predating) { t.Fatalf("the env file that predated the import changed: %v", err) } if _, err := os.Stat(adoption); err != nil { t.Fatalf("the adoption record that predated the import went: %v", err) } }) t.Run("an env file with no key", func(t *testing.T) { noOwnReplica(t) b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{}) dir := provision.InstanceDir(b.DataDir, rt.Host) if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } envPath := filepath.Join(dir, rt.Host+".env") // No trailing newline, a comment, and a line the platform does not own. predating := "# set by hand\n" + token + "\nOTHER=1" if err := os.WriteFile(envPath, []byte(predating), 0o600); err != nil { t.Fatal(err) } adoption := filepath.Join(dir, rt.Host+instancekey.AdoptionSuffix) if err := os.WriteFile(adoption, []byte("{}\n"), 0o600); err != nil { t.Fatal(err) } before, err := SeedEnvBefore(rt.DBPath, rt.Host) if err != nil || before != registry.EnvBeforeNoKey { t.Fatalf("SeedEnvBefore = %q, %v; want no-key", before, err) } handed, own := launchKey(t, b, importedRoute(t, b, rt)) if !wellFormedInstanceKey(handed) || own != handed { t.Fatal("the import's launch did not mint a key into the file") } if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil { t.Fatal(err) } got, err := os.ReadFile(envPath) if err != nil { t.Fatal(err) } // Every line that was there is there, in order; the key is not. if string(got) != predating+"\n" { t.Fatalf("after the retirement the file is not what predated the import plus its newline (%d bytes, want %d)", len(got), len(predating)+1) } if st, _ := os.Stat(envPath); st.Mode().Perm() != 0o600 { t.Fatalf("mode = %o, want 600", st.Mode().Perm()) } if _, err := os.Stat(adoption); err != nil { t.Fatalf("the adoption record went with a file that predated the import: %v", err) } if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil { t.Fatal(err) } if again, _ := os.ReadFile(envPath); string(again) != string(got) { t.Fatal("a second retirement changed the file") } }) t.Run("a journal row that does not say", func(t *testing.T) { noOwnReplica(t) b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{}) handed, own := launchKey(t, b, importedRoute(t, b, rt)) if err := RetireSeedKey(rt.DBPath, rt.Host, ""); err != nil { t.Fatal(err) } if _, now := launchKeyFile(t, b, rt); now != own || own != handed { t.Fatal("a key was retired on no record of what predated the import") } }) t.Run("nothing there", func(t *testing.T) { b, rt := nestedBackend(t, &fakeControl{}, &fakeUnit{}) for _, before := range []string{registry.EnvBeforeAbsent, registry.EnvBeforeNoKey, registry.EnvBeforeKey, ""} { if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil { t.Fatalf("%q with no instance directory: %v", before, err) } } if err := RetireSeedKey(rt.DBPath, rt.Host, "whatever"); err == nil { t.Fatal("an unknown env state was accepted") } _ = b }) } // launchKeyFile reads the own env file's key without launching. func launchKeyFile(t *testing.T, b *ExecBackend, rt registry.Route) (exists bool, key string) { t.Helper() raw, err := os.ReadFile(filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), rt.Host+".env")) if os.IsNotExist(err) { return false, "" } if err != nil { t.Fatal(err) } return true, parseEnvBytes(raw)[InstanceKeyVar] } // The swap does not go ahead on an env file it cannot read: a link there is // refused, never followed, and never read as "no file". func TestSeedEnvBeforeRefusesALink(t *testing.T) { b, rt := nestedBackend(t, &fakeControl{}, &fakeUnit{}) dir := provision.InstanceDir(b.DataDir, rt.Host) if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } target := filepath.Join(t.TempDir(), "elsewhere.env") if err := os.WriteFile(target, []byte(InstanceKeyVar+"=x\n"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink(target, filepath.Join(dir, rt.Host+".env")); err != nil { t.Fatal(err) } if before, err := SeedEnvBefore(rt.DBPath, rt.Host); err == nil { t.Fatalf("SeedEnvBefore = %q over a symlink, want an error", before) } }