//go:build linux package host import ( "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "syscall" "testing" "github.com/carlosframework/platform/internal/hostbox" ) func TestR2InventoryNamesForeignArtifact(t *testing.T) { t.Parallel() for _, rootName := range []string{"opt", "units", "data"} { t.Run(rootName, func(t *testing.T) { root, target, reg, base := plan9Fix1RootRetire(t) row, err := reg.Get(target.Host) if err != nil { t.Fatal(err) } row.DBPath = filepath.Join(base, "instances", target.Host, target.Host+".db") if err = reg.Upsert(*row); err != nil { t.Fatal(err) } root.Roots[rootName] = base name := "carlos" if rootName == "units" { name = "carlos-edge.service.d/guard.conf" } if rootName == "data" { name = "instances/another-host/secret" } victim := filepath.Join(base, name) if err = os.MkdirAll(filepath.Dir(victim), 0700); err != nil { t.Fatal(err) } if err = os.WriteFile(victim, []byte("root-only"), 0600); err != nil { t.Fatal(err) } var st syscall.Stat_t if err = syscall.Lstat(victim, &st); err != nil { t.Fatal(err) } in, err := root.Reconcile.Inventory.ReadExactStopInventory(t.Context(), target) if err != nil { t.Fatal(err) } in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, base, name), Kind: "path", ArtifactID: "foreign", ManagedRoot: rootName, RelativePath: name, Ownership: row.ClaimOwner, Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & syscall.S_IFMT}) // Simulate a durable pre-fix or migrated agent inventory. if err = root.Reconcile.Inventory.WriteStopInventory(t.Context(), in); err != nil { t.Fatal(err) } err = root.retire(t.Context(), target) if !errors.Is(err, hostbox.ErrTypedInventory) { t.Errorf("foreign inventory not refused at retire: %v", err) } if raw, e := os.ReadFile(victim); e != nil || string(raw) != "root-only" { t.Errorf("root moved foreign file: %v", e) } j, e := hostbox.OpenLaunchJournal(filepath.Join(base, "service.db")) if e != nil { t.Fatal(e) } defer j.Close() root.Journals, e = hostbox.NewJournalService(j) if e != nil { t.Fatal(e) } root.Reconcile.Box, root.Reconcile.Enrollment = target.Box, target.Enrollment f := newRecoveryFixture(t) c := f.read().Claim root.Stop.Coordinator.Evidence = f.claims in.Host, in.Incarnation, in.InstallationStamp = c.Host, c.Holder.Incarnation, c.Holder.Stamp row.Host, row.ClaimIncarnation, row.ClaimStamp = in.Host, in.Incarnation, in.InstallationStamp row.DBPath = filepath.Join(base, "instances", in.Host, in.Host+".db") in.Entries[0].RelativePath = in.Host if e = reg.Upsert(*row); e != nil { t.Fatal(e) } target.Host, target.Incarnation, target.Stamp = in.Host, in.Incarnation, in.InstallationStamp var q hostbox.JournalRequest _, remote := hostbox.RemoteJournals(func(_ context.Context, request hostbox.JournalRequest) (hostbox.JournalResponse, error) { q = request return hostbox.JournalResponse{V: 1, ID: request.ID}, nil }) if e = remote.WriteStopInventory(t.Context(), in); e != nil { t.Fatal(e) } body, _ := json.Marshal(q) response := httptest.NewRecorder() root.serveJournal(response, httptest.NewRequest(http.MethodPost, "/claim/journal", bytes.NewReader(body)), StopPeer{}) var out hostbox.JournalResponse _ = json.Unmarshal(response.Body.Bytes(), &out) if response.Code == http.StatusOK && out.Error == "" { t.Error("foreign inventory accepted at write") } if _, e = root.Journals.Inventory.ReadExactStopInventory(t.Context(), target); e == nil { t.Error("foreign inventory became durable") } in.Entries = in.Entries[:1] in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ManagedRoot: "data", RelativePath: filepath.Join("instances", in.Host, in.Host+".db"), ArtifactID: "state", Ownership: row.ClaimOwner, Materialization: "intended"}) if e = remote.WriteStopInventory(t.Context(), in); e != nil { t.Fatal(e) } body, _ = json.Marshal(q) response = httptest.NewRecorder() root.serveJournal(response, httptest.NewRequest(http.MethodPost, "/claim/journal", bytes.NewReader(body)), StopPeer{}) out = hostbox.JournalResponse{} if e = json.Unmarshal(response.Body.Bytes(), &out); e != nil || response.Code != http.StatusOK || out.Error != "" { t.Fatalf("own inventory refused: %d %+v %v", response.Code, out, e) } if _, e = root.Journals.Inventory.ReadExactStopInventory(t.Context(), target); e != nil { t.Fatalf("own inventory not durable: %v", e) } }) } } func TestPlan9Fix2RetireOwnStateAndRecoverArchivedRoute(t *testing.T) { root, target, reg, base := plan9Fix1RootRetire(t) row, err := reg.Get(target.Host) if err != nil { t.Fatal(err) } state := filepath.Join(base, "instances", target.Host) row.DBPath = filepath.Join(state, target.Host+".db") if err = reg.Upsert(*row); err != nil { t.Fatal(err) } if err = os.MkdirAll(state, 0700); err != nil { t.Fatal(err) } if err = os.WriteFile(row.DBPath, []byte("own-state"), 0600); err != nil { t.Fatal(err) } var st syscall.Stat_t if err = syscall.Lstat(row.DBPath, &st); err != nil { t.Fatal(err) } in, err := root.Reconcile.Inventory.ReadExactStopInventory(t.Context(), target) if err != nil { t.Fatal(err) } in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, base, filepath.Join("instances", target.Host, target.Host+".db")), Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: filepath.Join("instances", target.Host, target.Host+".db"), Ownership: row.ClaimOwner, Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & syscall.S_IFMT}) if err = root.Reconcile.Inventory.WriteStopInventory(t.Context(), in); err != nil { t.Fatal(err) } // This fixture deliberately fails only the final remote acknowledgement. first := root.retire(t.Context(), target) if !errors.Is(first, ErrClaimStopIdentity) { t.Fatalf("own-state retirement: %v", first) } if _, err = os.Stat(row.DBPath); !errors.Is(err, os.ErrNotExist) { t.Fatalf("own state not parked: %v", err) } if row, err = reg.Get(target.Host); err != nil || row != nil { t.Fatalf("route not removed: %+v %v", row, err) } // Recovery gets its DB path from root's archive after the live row is gone. if err = root.retire(t.Context(), target); !errors.Is(err, ErrClaimStopIdentity) { t.Fatalf("archive recovery: %v", err) } }