feat(engine): add RunOptions.Force to skip Detect and call PlanForce
This commit is contained in:
@@ -14,7 +14,10 @@ type RunOptions struct {
|
|||||||
Selected []string // module IDs to run; empty = all
|
Selected []string // module IDs to run; empty = all
|
||||||
DryRun bool // Detect + Plan only, no Apply/Verify
|
DryRun bool // Detect + Plan only, no Apply/Verify
|
||||||
HaltOnError bool // stop at first failure instead of continuing siblings
|
HaltOnError bool // stop at first failure instead of continuing siblings
|
||||||
Reporter module.Reporter
|
// Force, when true, skips Detect and calls PlanForce on every module
|
||||||
|
// so each one produces a from-scratch plan regardless of current state.
|
||||||
|
Force bool
|
||||||
|
Reporter module.Reporter
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModuleStatus is the terminal state of a module within a run.
|
// ModuleStatus is the terminal state of a module within a run.
|
||||||
@@ -91,26 +94,43 @@ func Run(ctx context.Context, reg *module.Registry, opts RunOptions) (Summary, e
|
|||||||
res := &ModuleResult{ID: id}
|
res := &ModuleResult{ID: id}
|
||||||
results[id] = res
|
results[id] = res
|
||||||
|
|
||||||
state, err := m.Detect(ctx)
|
var (
|
||||||
if err != nil {
|
plan module.Plan
|
||||||
res.Status = StatusFailed
|
err error
|
||||||
res.Err = fmt.Errorf("detect: %w", err)
|
)
|
||||||
markFailed(metas, id, skipped)
|
if opts.Force {
|
||||||
if opts.HaltOnError {
|
plan, err = m.PlanForce(ctx)
|
||||||
break
|
if err != nil {
|
||||||
|
res.Status = StatusFailed
|
||||||
|
res.Err = fmt.Errorf("plan-force: %w", err)
|
||||||
|
markFailed(metas, id, skipped)
|
||||||
|
if opts.HaltOnError {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state, derr := m.Detect(ctx)
|
||||||
|
if derr != nil {
|
||||||
|
res.Status = StatusFailed
|
||||||
|
res.Err = fmt.Errorf("detect: %w", derr)
|
||||||
|
markFailed(metas, id, skipped)
|
||||||
|
if opts.HaltOnError {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
plan, err := m.Plan(ctx, state)
|
plan, err = m.Plan(ctx, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
res.Status = StatusFailed
|
res.Status = StatusFailed
|
||||||
res.Err = fmt.Errorf("plan: %w", err)
|
res.Err = fmt.Errorf("plan: %w", err)
|
||||||
markFailed(metas, id, skipped)
|
markFailed(metas, id, skipped)
|
||||||
if opts.HaltOnError {
|
if opts.HaltOnError {
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
res.Plan = plan
|
res.Plan = plan
|
||||||
|
|
||||||
|
|||||||
@@ -10,40 +10,51 @@ import (
|
|||||||
|
|
||||||
// scriptedModule lets tests control what each phase returns.
|
// scriptedModule lets tests control what each phase returns.
|
||||||
type scriptedModule struct {
|
type scriptedModule struct {
|
||||||
id string
|
id string
|
||||||
deps []string
|
deps []string
|
||||||
detect func() (module.State, error)
|
detect func() (module.State, error)
|
||||||
plan func() (module.Plan, error)
|
plan func() (module.Plan, error)
|
||||||
apply func() error
|
planForce func() (module.Plan, error)
|
||||||
verify func() error
|
apply func() error
|
||||||
|
verify func() error
|
||||||
|
|
||||||
|
detectCalls int
|
||||||
|
planCalls int
|
||||||
|
planForceCalls int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s scriptedModule) Meta() module.Meta { return module.Meta{ID: s.id, Name: s.id, Deps: s.deps} }
|
func (s *scriptedModule) Meta() module.Meta { return module.Meta{ID: s.id, Name: s.id, Deps: s.deps} }
|
||||||
func (s scriptedModule) Detect(ctx context.Context) (module.State, error) {
|
func (s *scriptedModule) Detect(ctx context.Context) (module.State, error) {
|
||||||
|
s.detectCalls++
|
||||||
if s.detect != nil {
|
if s.detect != nil {
|
||||||
return s.detect()
|
return s.detect()
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (s scriptedModule) Plan(ctx context.Context, _ module.State) (module.Plan, error) {
|
func (s *scriptedModule) Plan(ctx context.Context, _ module.State) (module.Plan, error) {
|
||||||
|
s.planCalls++
|
||||||
if s.plan != nil {
|
if s.plan != nil {
|
||||||
return s.plan()
|
return s.plan()
|
||||||
}
|
}
|
||||||
return module.Plan{}, nil
|
return module.Plan{}, nil
|
||||||
}
|
}
|
||||||
func (s scriptedModule) PlanForce(ctx context.Context) (module.Plan, error) {
|
func (s *scriptedModule) PlanForce(ctx context.Context) (module.Plan, error) {
|
||||||
|
s.planForceCalls++
|
||||||
|
if s.planForce != nil {
|
||||||
|
return s.planForce()
|
||||||
|
}
|
||||||
if s.plan != nil {
|
if s.plan != nil {
|
||||||
return s.plan()
|
return s.plan()
|
||||||
}
|
}
|
||||||
return module.Plan{}, nil
|
return module.Plan{}, nil
|
||||||
}
|
}
|
||||||
func (s scriptedModule) Apply(ctx context.Context, _ module.Plan, _ module.Reporter) error {
|
func (s *scriptedModule) Apply(ctx context.Context, _ module.Plan, _ module.Reporter) error {
|
||||||
if s.apply != nil {
|
if s.apply != nil {
|
||||||
return s.apply()
|
return s.apply()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s scriptedModule) Verify(ctx context.Context) error {
|
func (s *scriptedModule) Verify(ctx context.Context) error {
|
||||||
if s.verify != nil {
|
if s.verify != nil {
|
||||||
return s.verify()
|
return s.verify()
|
||||||
}
|
}
|
||||||
@@ -63,8 +74,8 @@ func newReg(t *testing.T, mods ...module.Module) *module.Registry {
|
|||||||
|
|
||||||
func TestRunAllSucceed(t *testing.T) {
|
func TestRunAllSucceed(t *testing.T) {
|
||||||
reg := newReg(t,
|
reg := newReg(t,
|
||||||
scriptedModule{id: "a"},
|
&scriptedModule{id: "a"},
|
||||||
scriptedModule{id: "b", deps: []string{"a"}},
|
&scriptedModule{id: "b", deps: []string{"a"}},
|
||||||
)
|
)
|
||||||
sum, err := Run(context.Background(), reg, RunOptions{Reporter: NewBufferedReporter()})
|
sum, err := Run(context.Background(), reg, RunOptions{Reporter: NewBufferedReporter()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,7 +92,7 @@ func TestRunAllSucceed(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunDryRunDoesNotApply(t *testing.T) {
|
func TestRunDryRunDoesNotApply(t *testing.T) {
|
||||||
applied := false
|
applied := false
|
||||||
reg := newReg(t, scriptedModule{
|
reg := newReg(t, &scriptedModule{
|
||||||
id: "a",
|
id: "a",
|
||||||
apply: func() error { applied = true; return nil },
|
apply: func() error { applied = true; return nil },
|
||||||
})
|
})
|
||||||
@@ -99,10 +110,10 @@ func TestRunDryRunDoesNotApply(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunFailureSkipsDescendantsContinuesSiblings(t *testing.T) {
|
func TestRunFailureSkipsDescendantsContinuesSiblings(t *testing.T) {
|
||||||
reg := newReg(t,
|
reg := newReg(t,
|
||||||
scriptedModule{id: "root"},
|
&scriptedModule{id: "root"},
|
||||||
scriptedModule{id: "fails", deps: []string{"root"}, apply: func() error { return errors.New("boom") }},
|
&scriptedModule{id: "fails", deps: []string{"root"}, apply: func() error { return errors.New("boom") }},
|
||||||
scriptedModule{id: "child", deps: []string{"fails"}},
|
&scriptedModule{id: "child", deps: []string{"fails"}},
|
||||||
scriptedModule{id: "sibling", deps: []string{"root"}},
|
&scriptedModule{id: "sibling", deps: []string{"root"}},
|
||||||
)
|
)
|
||||||
sum, err := Run(context.Background(), reg, RunOptions{Reporter: NewBufferedReporter()})
|
sum, err := Run(context.Background(), reg, RunOptions{Reporter: NewBufferedReporter()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -128,8 +139,8 @@ func TestRunFailureSkipsDescendantsContinuesSiblings(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunHaltOnError(t *testing.T) {
|
func TestRunHaltOnError(t *testing.T) {
|
||||||
reg := newReg(t,
|
reg := newReg(t,
|
||||||
scriptedModule{id: "a", apply: func() error { return errors.New("nope") }},
|
&scriptedModule{id: "a", apply: func() error { return errors.New("nope") }},
|
||||||
scriptedModule{id: "b"}, // independent
|
&scriptedModule{id: "b"}, // independent
|
||||||
)
|
)
|
||||||
sum, err := Run(context.Background(), reg, RunOptions{HaltOnError: true, Reporter: NewBufferedReporter()})
|
sum, err := Run(context.Background(), reg, RunOptions{HaltOnError: true, Reporter: NewBufferedReporter()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -150,3 +161,36 @@ func TestRunUnknownModule(t *testing.T) {
|
|||||||
t.Fatal("expected error for unknown module")
|
t.Fatal("expected error for unknown module")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunForceSkipsDetectAndCallsPlanForce(t *testing.T) {
|
||||||
|
mod := &scriptedModule{
|
||||||
|
id: "a",
|
||||||
|
detect: func() (module.State, error) {
|
||||||
|
panic("Detect must not be called when Force=true")
|
||||||
|
},
|
||||||
|
planForce: func() (module.Plan, error) {
|
||||||
|
return module.Plan{Actions: []module.Action{{Description: "forced"}}}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reg := newReg(t, mod)
|
||||||
|
sum, err := Run(context.Background(), reg, RunOptions{Force: true, Reporter: NewBufferedReporter()})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if mod.planForceCalls != 1 {
|
||||||
|
t.Errorf("planForceCalls = %d, want 1", mod.planForceCalls)
|
||||||
|
}
|
||||||
|
if mod.detectCalls != 0 {
|
||||||
|
t.Errorf("detectCalls = %d, want 0", mod.detectCalls)
|
||||||
|
}
|
||||||
|
if mod.planCalls != 0 {
|
||||||
|
t.Errorf("planCalls = %d, want 0", mod.planCalls)
|
||||||
|
}
|
||||||
|
ok, fail, skip := sum.Counts()
|
||||||
|
if ok != 1 || fail != 0 || skip != 0 {
|
||||||
|
t.Fatalf("counts = (%d,%d,%d)", ok, fail, skip)
|
||||||
|
}
|
||||||
|
if len(sum.Results[0].Plan.Actions) != 1 {
|
||||||
|
t.Fatalf("expected 1 action in plan, got %d", len(sum.Results[0].Plan.Actions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user