-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
runner: runner initializes and runs tasks for flasher
- Loading branch information
Showing
3 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package runner | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/metal-toolbox/flasher/internal/model" | ||
"github.com/pkg/errors" | ||
"github.com/sirupsen/logrus" | ||
) | ||
|
||
// A Runner instance runs a single task, setting up and executing the actions required to install firmware | ||
// on one or more server components. | ||
type Runner struct { | ||
logger *logrus.Entry | ||
} | ||
|
||
type Handler interface { | ||
Initialize(ctx context.Context) error | ||
Query(ctx context.Context) error | ||
PlanActions(ctx context.Context) error | ||
RunActions(ctx context.Context) error | ||
OnSuccess(ctx context.Context, task *model.Task) | ||
OnFailure(ctx context.Context, task *model.Task) | ||
Publish() | ||
} | ||
|
||
func New(logger *logrus.Entry) *Runner { | ||
return &Runner{ | ||
logger: logger, | ||
} | ||
} | ||
|
||
func (r *Runner) RunTask(ctx context.Context, task *model.Task, handler Handler) error { | ||
funcs := map[string]func(context.Context) error{ | ||
"Initialize": handler.Initialize, | ||
"Query": handler.Query, | ||
"PlanActions": handler.PlanActions, | ||
"RunActions": handler.RunActions, | ||
} | ||
|
||
taskFailed := func(err error) error { | ||
// no error returned | ||
_ = task.SetState(model.StateFailed) | ||
task.Status.Append("task failed") | ||
task.Status.Append(err.Error()) | ||
handler.Publish() | ||
|
||
handler.OnFailure(ctx, task) | ||
|
||
return err | ||
} | ||
|
||
taskSuccess := func() error { | ||
// no error returned | ||
_ = task.SetState(model.StateSucceeded) | ||
task.Status.Append("task completed successfully") | ||
handler.Publish() | ||
|
||
handler.OnSuccess(ctx, task) | ||
|
||
return nil | ||
} | ||
|
||
// no error returned | ||
_ = task.SetState(model.StateActive) | ||
handler.Publish() | ||
|
||
for fname, f := range funcs { | ||
if cferr := r.conditionalFault(fname, task, handler); cferr != nil { | ||
return taskFailed(cferr) | ||
} | ||
|
||
if err := f(ctx); err != nil { | ||
return taskFailed(err) | ||
} | ||
} | ||
|
||
return taskSuccess() | ||
} | ||
|
||
// conditionalFault is invoked before each runner method to induce a fault if specified | ||
func (r *Runner) conditionalFault(fname string, task *model.Task, handler Handler) error { | ||
var errConditionFault = errors.New("condition induced fault") | ||
|
||
if task.Fault == nil { | ||
return nil | ||
} | ||
|
||
if task.Fault.Panic { | ||
panic("condition induced panic..") | ||
} | ||
|
||
if task.Fault.FailAt == fname { | ||
return errors.Wrap(errConditionFault, fname) | ||
} | ||
|
||
if task.Fault.DelayDuration != "" { | ||
td, err := time.ParseDuration(task.Fault.DelayDuration) | ||
if err != nil { | ||
// invalid duration string is ignored | ||
return nil | ||
} | ||
|
||
task.Status.Append("condition induced delay: " + td.String()) | ||
handler.Publish() | ||
|
||
r.logger.WithField("delay", td.String()).Warn("condition induced delay in execution") | ||
time.Sleep(td) | ||
|
||
// purge delay duration string, this is to execute only once | ||
// and this method is called at each transition. | ||
task.Fault.DelayDuration = "" | ||
} | ||
|
||
return nil | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package runner | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/metal-toolbox/flasher/internal/model" | ||
"github.com/pkg/errors" | ||
"github.com/sirupsen/logrus" | ||
"github.com/stretchr/testify/assert" | ||
"go.uber.org/mock/gomock" | ||
) | ||
|
||
func TestRunTask(t *testing.T) { | ||
logger := logrus.New() | ||
logger.SetLevel(logrus.ErrorLevel) | ||
|
||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
mockHandler := NewMockHandler(ctrl) | ||
|
||
tests := []struct { | ||
name string | ||
setupMock func() | ||
expectedState string | ||
expectedError error | ||
}{ | ||
{ | ||
name: "Successful execution", | ||
setupMock: func() { | ||
mockHandler.EXPECT().Initialize(gomock.Any()).Return(nil) | ||
mockHandler.EXPECT().Query(gomock.Any()).Return(nil) | ||
mockHandler.EXPECT().PlanActions(gomock.Any()).Return(nil) | ||
mockHandler.EXPECT().RunActions(gomock.Any()).Return(nil) | ||
mockHandler.EXPECT().OnSuccess(gomock.Any(), gomock.Any()) | ||
mockHandler.EXPECT().Publish().AnyTimes() | ||
}, | ||
expectedState: string(model.StateSucceeded), | ||
expectedError: nil, | ||
}, | ||
{ | ||
name: "Failure during Initialize", | ||
setupMock: func() { | ||
mockHandler.EXPECT().Initialize(gomock.Any()).Return(errors.New("Initialize failed")) | ||
mockHandler.EXPECT().OnFailure(gomock.Any(), gomock.Any()) | ||
}, | ||
expectedState: string(model.StateFailed), | ||
expectedError: errors.New("Initialize failed"), | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
tt.setupMock() | ||
|
||
r := New(logrus.NewEntry(logrus.New())) | ||
task := &model.Task{} | ||
err := r.RunTask(context.Background(), task, mockHandler) | ||
|
||
if string(task.State()) != tt.expectedState { | ||
t.Errorf("Expected task state %s, but got %s", tt.expectedState, task.State()) | ||
} | ||
|
||
if tt.expectedError != nil { | ||
assert.EqualError(t, tt.expectedError, err.Error()) | ||
} | ||
|
||
if tt.expectedState == string(model.StateSucceeded) { | ||
assert.Nil(t, err) | ||
} | ||
}) | ||
} | ||
} |