//go:build linux && rootrunner package host import ( "context" "crypto/ed25519" "crypto/sha256" "errors" "io" "io/fs" "net" "net/http" "os" "path/filepath" "reflect" "strings" "testing" "time" "github.com/carlosframework/platform/internal/controlsock" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) func lifecycleActiveSibling(t *testing.T) { t.Helper() s, err := openClaimRootAuthority() lifecycleCheck(t, "sibling authority", err) defer s.Close() views, err := hostguard.NewRootAuthority(s).GenerationInstallations(t.Context()) lifecycleCheck(t, "sibling installations", err) for _, view := range views { if view.Host != lifecycleSibling { continue } if view.Installation.Stamp == 0 || view.Barrier != nil { t.Fatal("sibling is not active") } for _, old := range view.Retired { if old.Incarnation == view.Installation.Incarnation && old.Stamp == view.Installation.Stamp { t.Fatal("sibling retired") } } transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, "unix", "/run/carlos/instances/"+lifecycleSibling+"/app.sock") }} defer transport.CloseIdleConnections() response, err := (&http.Client{Transport: transport, Timeout: time.Second}).Get("http://sibling/") lifecycleCheck(t, "active sibling response", err) defer response.Body.Close() raw, err := io.ReadAll(response.Body) lifecycleCheck(t, "active sibling body", err) if response.StatusCode != http.StatusOK || string(raw) != "lifecycle tenant" { t.Fatal("sibling stopped serving", response.StatusCode, string(raw)) } return } t.Fatal("missing production sibling installation") } func lifecycleRetainedBytes(t *testing.T) map[string][32]byte { t.Helper() out := map[string][32]byte{} for _, root := range []string{"/var/lib/carlos-claims", "/data/.parked"} { lifecycleCheck(t, "retained evidence", filepath.WalkDir(root, func(path string, e fs.DirEntry, err error) error { if err != nil { return err } if !e.Type().IsRegular() || strings.HasSuffix(path, ".lock") { return nil } raw, err := os.ReadFile(path) if err != nil { return err } if strings.HasPrefix(path, "/data/.parked/") || strings.Contains(string(raw), lifecycleHost) { out[path] = sha256.Sum256(raw) } return nil })) } return out } func lifecycleReinstall(t *testing.T, store release.Store, key ed25519.PrivateKey, actor hostbox.Actor, pointer release.SystemPointer) { t.Helper() lifecycleActiveSibling(t) removed, err := domains.DetachClaim(t.Context(), store, lifecycleHost, "bab", "lifecycle") lifecycleCheck(t, "release deleted instance domain", err) if !removed { t.Fatal("deleted instance domain was not released") } before := lifecycleRetainedBytes(t) lifecycleCreate(t, store, key, actor, pointer, provision.Record{Schema: 1, Host: lifecycleHost, Account: "bab", App: "reinstall", Channel: "stable", CreatedAt: time.Now().UTC()}) lifecycleCheck(t, "request reinstall", os.WriteFile("/run/carlos/lifecycle-reinstall", []byte("ready"), 0644)) deadline := time.Now().Add(time.Minute) for { if _, err := os.Stat("/run/carlos/lifecycle-reinstall-refused"); err == nil { break } if time.Now().After(deadline) { t.Fatal("reinstall refusal timeout") } time.Sleep(20 * time.Millisecond) } if !reflect.DeepEqual(before, lifecycleRetainedBytes(t)) { t.Fatal("reinstall changed retained evidence") } lifecycleActiveSibling(t) } func lifecycleAgentReinstall(t *testing.T, ctx context.Context, store release.Store) { t.Helper() deadline := time.Now().Add(2 * time.Minute) refused := false for time.Now().Before(deadline) { if _, err := os.Stat("/run/carlos/lifecycle-finish"); err == nil { return } if _, err := os.Stat("/run/carlos/lifecycle-reinstall"); err == nil && !refused { obs, err := hostclaim.Read(ctx, store, lifecycleHost) lifecycleCheck(t, "reinstall claim", err) q := executionExpectation(obs.Claim) q.ID, q.Operation = "reinstall", "executionView" _, err = rootExecutionClient(controlsock.Path)(ctx, q) want := lifecycleHost + ": hostname was retired on this box; reinstalling it is not yet supported" if err == nil || errors.Is(err, execlease.ErrLost) || err.Error() != want { t.Fatalf("reinstall refusal: %v", err) } lifecycleCheck(t, "reinstall refused", os.WriteFile("/run/carlos/lifecycle-reinstall-refused", []byte("ready"), 0644)) refused = true } time.Sleep(20 * time.Millisecond) } t.Fatal("lifecycle verification timeout") }