package host import ( "context" "errors" "os/exec" "reflect" "testing" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/registry" ) func TestFilesSeedPreparationOrdersAndSettles(t *testing.T) { for _, failure := range []string{"none", "prerequisites", "admit", "prepare", "restore", "reserve", "materialize", "settle"} { t.Run(failure, func(t *testing.T) { var events []string mark := func(s string) error { events = append(events, s) if failure == s { return hostbox.ErrChanged } return nil } obs := hostclaim.Observed{Host: recoveryHost, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc"}}} svc := &activator.Service{} token, err := svc.AcquireSeedExclusion(t.Context(), obs.Host, nil) if err != nil { t.Fatal(err) } r := &activator.ClaimRuntime{ Read: func(context.Context, string) (hostclaim.Observed, error) { return obs, nil }, Prerequisites: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) error { return mark("prerequisites") }, Admit: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (hostclaim.Observed, error) { return obs, mark("admit") }, FailedAdmission: func(ctx context.Context, _ hostclaim.Observed, _ error) error { if ctx.Err() != nil { t.Fatal("canceled cleanup") } return mark("cleanup") }, Prepare: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (activator.ClaimStart, error) { if err := mark("prepare"); err != nil { return activator.ClaimStart{}, err } return activator.ClaimStart{Restore: func(context.Context) error { return mark("restore") }, Reserve: func(_ context.Context, n int) error { if n != 0 { t.Fatal("additional attempt") } return mark("reserve") }, Materialize: func(context.Context) error { return mark("materialize") }, Failed: func(context.Context, int) error { return mark("settle") }, Build: func() *exec.Cmd { t.Fatal("seed launched tenant"); return nil }, Healthy: func(context.Context, int, *exec.Cmd) error { t.Fatal("seed became healthy"); return nil }}, nil }, } _, settled, err := prepareFilesSeed(t.Context(), registry.Route{Host: obs.Host}, r, obs, token) if failure == "none" { if err != nil || !settled { t.Fatal(err, settled) } want := []string{"prerequisites", "admit", "prepare", "restore", "reserve", "materialize", "settle", "cleanup"} if !reflect.DeepEqual(events, want) { t.Fatal(events) } } else if err == nil || settled == (failure == "settle") { t.Fatal("wrong cleanup outcome", events, err, settled) } if err := token.Check(obs.Host); err != nil { t.Fatal("driver released caller's fence", err) } }) } } func TestFilesSeedCommandRefusesBeforeConstruction(t *testing.T) { for _, args := range [][]string{{}, {"repair"}, {"seed", "--preflight"}, {"seed", "--host", "../bad"}, {"seed", "extra"}} { if _, err := parseFilesSeed(args); err == nil { t.Fatal(args) } } host, err := parseFilesSeed([]string{"seed", "--host", recoveryHost}) if err != nil || host != recoveryHost { t.Fatal(host, err) } t.Setenv("CARLOS_HOST_CLAIM", "") t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "") if err := runSystemFiles(t.Context(), []string{"seed"}, nil); err == nil { t.Fatal("flags off accepted") } t.Setenv("CARLOS_HOST_CLAIM", "1") t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1") if err := runSystemFiles(t.Context(), []string{"seed"}, nil); !errors.Is(err, errFilesSeedDelegation) { t.Fatal("shell reached construction", err) } } func TestFilesSeedRootCapabilityRequiresDelegationValidator(t *testing.T) { f := newLaunchFixture(t, hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"}) q := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchCapability, Kind: "files-seed"} if _, err := f.root.Serve(t.Context(), f.peer, q); err == nil { t.Fatal("unbound validator accepted") } called := 0 f.root.SeedController = func(_ context.Context, p StopPeer) error { called++ if p != f.peer { t.Fatal("caller invented peer") } return hostbox.ErrChanged } if _, err := f.root.Serve(t.Context(), f.peer, q); !errors.Is(err, hostbox.ErrChanged) || called != 1 { t.Fatal(err, called) } f.root.SeedController = func(context.Context, StopPeer) error { return nil } if _, err := f.root.Serve(t.Context(), f.peer, q); err != nil { t.Fatal(err) } if len(f.units.starts) != 0 { t.Fatal("capability launched tenant") } } func TestFilesSeedAmbiguousAdmissionRetainsExclusion(t *testing.T) { before := hostclaim.Observed{Host: recoveryHost, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc"}}} after := hostclaim.Observed{Host: recoveryHost, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc", WakeRetry: &hostclaim.WakeRetry{State: "attempting"}}}} service := &activator.Service{} token, err := service.AcquireSeedExclusion(t.Context(), recoveryHost, nil) if err != nil { t.Fatal(err) } runtime := &activator.ClaimRuntime{ Read: func(context.Context, string) (hostclaim.Observed, error) { return after, nil }, Prerequisites: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) error { return nil }, Admit: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (hostclaim.Observed, error) { return hostclaim.Observed{}, hostbox.ErrChanged }, Prepare: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (activator.ClaimStart, error) { t.Fatal("ambiguous admission prepared") return activator.ClaimStart{}, nil }, FailedAdmission: func(context.Context, hostclaim.Observed, error) error { return nil }, } _, settled, err := prepareFilesSeed(t.Context(), registry.Route{Host: recoveryHost}, runtime, before, token) if err == nil || settled || token.Check(recoveryHost) != nil { t.Fatal("ambiguous debt reported settled", err, settled) } } func TestFilesSeedExitCodes(t *testing.T) { if filesSeedExitCode(nil) != 0 || filesSeedExitCode(hostbox.ErrChanged) != 1 || filesSeedExitCode(filesSeedIncompleteError{hostbox.ErrChanged}) != 2 { t.Fatal("incorrect status") } } func TestFilesSeedPreparationCleanupSharesCanceledBudget(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) ctx, finish := activator.WithCleanupBudget(ctx, 50*time.Millisecond) defer finish() obs := hostclaim.Observed{Host: recoveryHost, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc"}}} token, err := (&activator.Service{}).AcquireSeedExclusion(ctx, recoveryHost, nil) if err != nil { t.Fatal(err) } var deadline time.Time runtime := &activator.ClaimRuntime{ Read: func(ctx context.Context, _ string) (hostclaim.Observed, error) { return obs, ctx.Err() }, Prerequisites: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) error { return nil }, Admit: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (hostclaim.Observed, error) { return obs, nil }, Prepare: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (activator.ClaimStart, error) { return activator.ClaimStart{Restore: func(context.Context) error { return nil }, Reserve: func(context.Context, int) error { return nil }, Materialize: func(context.Context) error { cancel(); return context.Canceled }, Failed: func(ctx context.Context, _ int) error { deadline, _ = ctx.Deadline(); <-ctx.Done(); return ctx.Err() }}, nil }, FailedAdmission: func(ctx context.Context, _ hostclaim.Observed, _ error) error { d, _ := ctx.Deadline() if !d.Equal(deadline) || ctx.Err() == nil { t.Error("cleanup received fresh budget") } return ctx.Err() }, } started := time.Now() _, settled, err := prepareFilesSeed(ctx, registry.Route{Host: recoveryHost}, runtime, obs, token) if err == nil || settled || time.Since(started) > time.Second || token.Check(recoveryHost) != nil { t.Fatal("unbounded or falsely settled cleanup", err, settled) } }