From e4f0080a1efc9887fe8aa892669b2002e70b6040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Brauer?= Date: Tue, 8 Feb 2022 21:17:59 +0100 Subject: [PATCH] refactor: extract setupAction into ActionReader (#986) This change extracts the functionality of reading an `action.y(a)ml` or creation of a `(Synthetic Action)` into its own type to enable better unit testing / mocking of those IO operations. This is done in preparation for the implementation of pre/post action support in act. Co-authored-by: Markus Wolf Co-authored-by: Markus Wolf --- pkg/runner/action.go | 84 +++++++++++++++++++++++ pkg/runner/action_test.go | 133 +++++++++++++++++++++++++++++++++++++ pkg/runner/step_context.go | 114 ++++++++----------------------- 3 files changed, 246 insertions(+), 85 deletions(-) create mode 100644 pkg/runner/action.go create mode 100644 pkg/runner/action_test.go diff --git a/pkg/runner/action.go b/pkg/runner/action.go new file mode 100644 index 0000000..b9024fb --- /dev/null +++ b/pkg/runner/action.go @@ -0,0 +1,84 @@ +package runner + +import ( + "embed" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/nektos/act/pkg/model" + log "github.com/sirupsen/logrus" +) + +type ActionReader interface { + readAction(step *model.Step, actionDir string, actionPath string, readFile actionyamlReader) (*model.Action, error) +} + +type actionyamlReader func(filename string) (io.Reader, io.Closer, error) +type fileWriter func(filename string, data []byte, perm fs.FileMode) error + +//go:embed res/trampoline.js +var trampoline embed.FS + +func (sc *StepContext) readAction(step *model.Step, actionDir string, actionPath string, readFile actionyamlReader, writeFile fileWriter) (*model.Action, error) { + reader, closer, err := readFile("action.yml") + if os.IsNotExist(err) { + reader, closer, err = readFile("action.yaml") + if err != nil { + if _, closer, err2 := readFile("Dockerfile"); err2 == nil { + closer.Close() + action := &model.Action{ + Name: "(Synthetic)", + Runs: model.ActionRuns{ + Using: "docker", + Image: "Dockerfile", + }, + } + log.Debugf("Using synthetic action %v for Dockerfile", action) + return action, nil + } + if step.With != nil { + if val, ok := step.With["args"]; ok { + var b []byte + if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil { + return nil, err + } + err2 := writeFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0400) + if err2 != nil { + return nil, err2 + } + action := &model.Action{ + Name: "(Synthetic)", + Inputs: map[string]model.Input{ + "cwd": { + Description: "(Actual working directory)", + Required: false, + Default: filepath.Join(actionDir, actionPath), + }, + "command": { + Description: "(Actual program)", + Required: false, + Default: val, + }, + }, + Runs: model.ActionRuns{ + Using: "node12", + Main: "trampoline.js", + }, + } + log.Debugf("Using synthetic action %v", action) + return action, nil + } + } + return nil, err + } + } else if err != nil { + return nil, err + } + defer closer.Close() + + action, err := model.ReadAction(reader) + log.Debugf("Read action %v from '%s'", action, "Unknown") + return action, err +} diff --git a/pkg/runner/action_test.go b/pkg/runner/action_test.go new file mode 100644 index 0000000..1edb80f --- /dev/null +++ b/pkg/runner/action_test.go @@ -0,0 +1,133 @@ +package runner + +import ( + "io" + "io/fs" + "strings" + "testing" + + "github.com/nektos/act/pkg/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type closerMock struct { + mock.Mock +} + +func (m *closerMock) Close() error { + m.Called() + return nil +} + +func TestActionReader(t *testing.T) { + yaml := strings.ReplaceAll(` +name: 'name' +runs: + using: 'node16' + main: 'main.js' +`, "\t", " ") + + table := []struct { + name string + step *model.Step + filename string + fileContent string + expected *model.Action + }{ + { + name: "readActionYml", + step: &model.Step{}, + filename: "action.yml", + fileContent: yaml, + expected: &model.Action{ + Name: "name", + Runs: model.ActionRuns{ + Using: "node16", + Main: "main.js", + }, + }, + }, + { + name: "readActionYaml", + step: &model.Step{}, + filename: "action.yaml", + fileContent: yaml, + expected: &model.Action{ + Name: "name", + Runs: model.ActionRuns{ + Using: "node16", + Main: "main.js", + }, + }, + }, + { + name: "readDockerfile", + step: &model.Step{}, + filename: "Dockerfile", + fileContent: "FROM ubuntu:20.04", + expected: &model.Action{ + Name: "(Synthetic)", + Runs: model.ActionRuns{ + Using: "docker", + Image: "Dockerfile", + }, + }, + }, + { + name: "readWithArgs", + step: &model.Step{ + With: map[string]string{ + "args": "cmd", + }, + }, + expected: &model.Action{ + Name: "(Synthetic)", + Inputs: map[string]model.Input{ + "cwd": { + Description: "(Actual working directory)", + Required: false, + Default: "actionDir/actionPath", + }, + "command": { + Description: "(Actual program)", + Required: false, + Default: "cmd", + }, + }, + Runs: model.ActionRuns{ + Using: "node12", + Main: "trampoline.js", + }, + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + closerMock := &closerMock{} + + readFile := func(filename string) (io.Reader, io.Closer, error) { + if tt.filename != filename { + return nil, nil, fs.ErrNotExist + } + + return strings.NewReader(tt.fileContent), closerMock, nil + } + + writeFile := func(filename string, data []byte, perm fs.FileMode) error { + assert.Equal(t, "actionDir/actionPath/trampoline.js", filename) + assert.Equal(t, fs.FileMode(0400), perm) + return nil + } + + closerMock.On("Close") + + sc := &StepContext{} + action, err := sc.readAction(tt.step, "actionDir", "actionPath", readFile, writeFile) + + assert.Nil(t, err) + assert.Equal(t, tt.expected, action) + }) + } +} diff --git a/pkg/runner/step_context.go b/pkg/runner/step_context.go index e5fd374..b9de160 100644 --- a/pkg/runner/step_context.go +++ b/pkg/runner/step_context.go @@ -3,7 +3,6 @@ package runner import ( "archive/tar" "context" - "embed" "fmt" "io" "io/ioutil" @@ -64,8 +63,24 @@ func (sc *StepContext) Executor(ctx context.Context) common.Executor { case model.StepTypeUsesActionLocal: actionDir := filepath.Join(rc.Config.Workdir, step.Uses) + + localReader := func(ctx context.Context) actionyamlReader { + _, cpath := sc.getContainerActionPaths(sc.Step, path.Join(actionDir, ""), sc.RunContext) + return func(filename string) (io.Reader, io.Closer, error) { + tars, err := sc.RunContext.JobContainer.GetContainerArchive(ctx, path.Join(cpath, filename)) + if err != nil { + return nil, nil, os.ErrNotExist + } + treader := tar.NewReader(tars) + if _, err := treader.Next(); err != nil { + return nil, nil, os.ErrNotExist + } + return treader, tars, nil + } + } + return common.NewPipelineExecutor( - sc.setupAction(actionDir, "", true), + sc.setupAction(actionDir, "", localReader), sc.runAction(actionDir, "", "", "", true), ) case model.StepTypeUsesActionRemote: @@ -102,9 +117,17 @@ func (sc *StepContext) Executor(ctx context.Context) common.Executor { ntErr = common.NewInfoExecutor("Non-terminating error while running 'git clone': %v", err) } } + + remoteReader := func(ctx context.Context) actionyamlReader { + return func(filename string) (io.Reader, io.Closer, error) { + f, err := os.Open(filepath.Join(actionDir, remoteAction.Path, filename)) + return f, f, err + } + } + return common.NewPipelineExecutor( ntErr, - sc.setupAction(actionDir, remoteAction.Path, false), + sc.setupAction(actionDir, remoteAction.Path, remoteReader), sc.runAction(actionDir, remoteAction.Path, remoteAction.Repo, remoteAction.Ref, false), ) case model.StepTypeInvalid: @@ -365,89 +388,10 @@ func (sc *StepContext) runUsesContainer() common.Executor { } } -//go:embed res/trampoline.js -var trampoline embed.FS - -func (sc *StepContext) setupAction(actionDir string, actionPath string, localAction bool) common.Executor { +func (sc *StepContext) setupAction(actionDir string, actionPath string, reader func(context.Context) actionyamlReader) common.Executor { return func(ctx context.Context) error { - var readFile func(filename string) (io.Reader, io.Closer, error) - if localAction { - _, cpath := sc.getContainerActionPaths(sc.Step, path.Join(actionDir, actionPath), sc.RunContext) - readFile = func(filename string) (io.Reader, io.Closer, error) { - tars, err := sc.RunContext.JobContainer.GetContainerArchive(ctx, path.Join(cpath, filename)) - if err != nil { - return nil, nil, os.ErrNotExist - } - treader := tar.NewReader(tars) - if _, err := treader.Next(); err != nil { - return nil, nil, os.ErrNotExist - } - return treader, tars, nil - } - } else { - readFile = func(filename string) (io.Reader, io.Closer, error) { - f, err := os.Open(filepath.Join(actionDir, actionPath, filename)) - return f, f, err - } - } - - reader, closer, err := readFile("action.yml") - if os.IsNotExist(err) { - reader, closer, err = readFile("action.yaml") - if err != nil { - if _, closer, err2 := readFile("Dockerfile"); err2 == nil { - closer.Close() - sc.Action = &model.Action{ - Name: "(Synthetic)", - Runs: model.ActionRuns{ - Using: "docker", - Image: "Dockerfile", - }, - } - log.Debugf("Using synthetic action %v for Dockerfile", sc.Action) - return nil - } - if sc.Step.With != nil { - if val, ok := sc.Step.With["args"]; ok { - var b []byte - if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil { - return err - } - err2 := ioutil.WriteFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0400) - if err2 != nil { - return err - } - sc.Action = &model.Action{ - Name: "(Synthetic)", - Inputs: map[string]model.Input{ - "cwd": { - Description: "(Actual working directory)", - Required: false, - Default: filepath.Join(actionDir, actionPath), - }, - "command": { - Description: "(Actual program)", - Required: false, - Default: val, - }, - }, - Runs: model.ActionRuns{ - Using: "node12", - Main: "trampoline.js", - }, - } - log.Debugf("Using synthetic action %v", sc.Action) - return nil - } - } - return err - } - } else if err != nil { - return err - } - defer closer.Close() - - sc.Action, err = model.ReadAction(reader) + action, err := sc.readAction(sc.Step, actionDir, actionPath, reader(ctx), ioutil.WriteFile) + sc.Action = action log.Debugf("Read action %v from '%s'", sc.Action, "Unknown") return err }