//go:build linux package cgroup import ( "errors" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" ) // The fixture has no kernel-generated control files. Preparation therefore // fails after the actual mkdir and before a launch reservation can be written. func TestLaunchChildFailureBeforeReservationSettlesAndRetries(t *testing.T) { ctx := t.Context() p := strictFixture(t) journalPath := filepath.Join(t.TempDir(), "launch.db") j, err := hostbox.OpenLaunchJournal(journalPath) if err != nil { t.Fatal(err) } defer func() { j.Close() }() target := hostguard.FenceTarget{Host: p.host, Incarnation: "inc", Stamp: 1, Session: 1, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "ctrl"} parent := hostbox.LaunchGroup{RelativePath: ExecName(p.host), Device: p.identity.device, Inode: p.identity.inode} if err := j.Prelaunch(ctx, hostbox.LaunchInventory{Target: target, ControllerPID: 42, ProcessStart: "start", Groups: []hostbox.LaunchGroup{parent}, NoState: true}); err != nil { t.Fatal(err) } reservation := func(name string) hostbox.LaunchReservation { return hostbox.LaunchReservation{Target: target, ID: name, Role: "main", InvocationID: name, Inputs: "inputs", Parent: parent, Group: hostbox.LaunchGroup{RelativePath: filepath.Join(parent.RelativePath, "role-"+name), InvocationID: name}, Executable: hostbox.LaunchFile{Device: 1, Inode: 2}} } r := reservation("first") childPath := filepath.Join(p.root.Name(), r.Group.RelativePath) called := false child, err := p.PrepareRoleChildWithIntent("first", func(id ContainmentIdentity) error { called = true if id.Device != parent.Device || id.Inode != parent.Inode { t.Fatal("wrong parent") } if _, err := os.Lstat(childPath); !errors.Is(err, os.ErrNotExist) { t.Fatal("child exists before intention") } return j.IntendRole(ctx, r) }) if err == nil || child != nil || !called { t.Fatalf("child=%v intent=%v err=%v", child, called, err) } if _, err := os.Stat(childPath); err != nil { t.Fatal("failure did not reach mkdir", err) } // Discard the writer's connection to model a crash before reservation. if err := j.Close(); err != nil { t.Fatal(err) } j, err = hostbox.OpenLaunchJournal(journalPath) if err != nil { t.Fatal(err) } stop, err := j.ReadStopKernelJournal(ctx, target) if err != nil || !stop.PendingRoles || stop.Settled { t.Fatalf("lost intention: %+v %v", stop, err) } if err := j.SettleRecovered(ctx, target); err == nil { t.Fatal("blind settlement accepted intended child") } // Kernel files are automatic on cgroupfs; supply the observation in this // filesystem fixture. Root requires emptiness before resolving the intention. if err := os.WriteFile(filepath.Join(childPath, "cgroup.events"), []byte("populated 1\n"), 0600); err != nil { t.Fatal(err) } if err := j.ResolveRoleIntentions(ctx, target, p.root.Name(), false); err == nil { t.Fatal("populated child accepted") } if err := os.WriteFile(filepath.Join(childPath, "cgroup.events"), []byte("populated 0\n"), 0600); err != nil { t.Fatal(err) } if err := j.ResolveRoleIntentions(ctx, target, p.root.Name(), false); err != nil { t.Fatal(err) } stop, err = j.ReadStopKernelJournal(ctx, target) if err != nil || stop.PendingRoles || !stop.Settled || len(stop.Cgroups) != 2 { t.Fatalf("unsettled child: %+v %v", stop, err) } // Retry retains the first attempt and uses a new child and invocation. next := reservation("second") if err := j.IntendRole(ctx, next); err != nil { t.Fatal(err) } nextPath := filepath.Join(p.root.Name(), next.Group.RelativePath) if err := os.Mkdir(nextPath, 0700); err != nil { t.Fatal(err) } f, err := os.Open(nextPath) if err != nil { t.Fatal(err) } id, err := strictID(f) f.Close() if err != nil { t.Fatal(err) } next.Group.Device, next.Group.Inode = id.device, id.inode if err := j.Reserve(ctx, next); err != nil { t.Fatal("retry reservation failed", err) } in, err := j.Read(ctx, target) if err != nil || len(in.Reservations) != 2 || in.Reservations[0].State != "settled" || in.Reservations[1].State != "reserved" { t.Fatalf("retry overwrote spent attempt: %+v %v", in, err) } } func TestLaunchChildIntentionFailureCreatesNothing(t *testing.T) { p := strictFixture(t) failure := errors.New("journal unavailable") _, err := p.PrepareRoleChildWithIntent("first", func(ContainmentIdentity) error { return failure }) if !errors.Is(err, failure) { t.Fatal(err) } if _, err := os.Stat(filepath.Join(p.dir.Name(), "role-first")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("child created before durable intention: %v", err) } }