Skip to content

Commit

Permalink
runner: runner initializes and runs tasks for flasher
Browse files Browse the repository at this point in the history
  • Loading branch information
joelrebel committed Feb 1, 2024
1 parent 6564c52 commit 4dfc36f
Show file tree
Hide file tree
Showing 3 changed files with 323 additions and 0 deletions.
117 changes: 117 additions & 0 deletions internal/runner/runner.go
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
}
132 changes: 132 additions & 0 deletions internal/runner/runner_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions internal/runner/runner_test.go
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)
}
})
}
}

0 comments on commit 4dfc36f

Please sign in to comment.