From 532af98aefbce84ec0de375cdfa0df813a84737a Mon Sep 17 00:00:00 2001 From: Casey Lee <cplee@nektos.com> Date: Thu, 6 Feb 2020 22:17:58 -0800 Subject: [PATCH] successfully able to run simple workflows Signed-off-by: Casey Lee <cplee@nektos.com> --- .github/workflows/basic.yml | 12 +- {pkg/runner => actions}/runner_exec.go | 9 +- cmd/graph.go | 3 +- cmd/input.go | 9 +- cmd/root.go | 43 +-- go.mod | 9 +- go.sum | 25 +- pkg/common/dryrun.go | 25 ++ pkg/common/executor.go | 169 ++++++++---- pkg/common/executor_test.go | 37 +-- pkg/common/git.go | 3 +- pkg/common/logger.go | 27 ++ pkg/container/docker_build.go | 19 +- pkg/container/docker_common.go | 115 -------- pkg/container/docker_logger.go | 117 ++++++++ pkg/container/docker_pull.go | 38 ++- pkg/container/docker_run.go | 356 +++++++++++++------------ pkg/container/docker_run_test.go | 14 +- pkg/model/planner.go | 17 ++ pkg/model/workflow.go | 42 ++- actions/log.go => pkg/runner/logger.go | 41 ++- pkg/runner/run_context.go | 269 +++++++++++++++++++ pkg/runner/runner.go | 54 ++-- 23 files changed, 958 insertions(+), 495 deletions(-) rename {pkg/runner => actions}/runner_exec.go (99%) create mode 100644 pkg/common/dryrun.go create mode 100644 pkg/common/logger.go delete mode 100644 pkg/container/docker_common.go create mode 100644 pkg/container/docker_logger.go rename actions/log.go => pkg/runner/logger.go (60%) create mode 100644 pkg/runner/run_context.go diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 7cdb3b4..ae962d0 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -4,5 +4,15 @@ on: push jobs: build: runs-on: ubuntu-latest + container: + image: node:10.16-jessie + env: + NODE_ENV: development steps: - - run: echo hello world! + - run: env + test: + runs-on: ubuntu-latest + steps: + - run: cp $GITHUB_EVENT_PATH $HOME/foo.json + - run: ls $HOME + - run: cat $HOME/foo.json diff --git a/pkg/runner/runner_exec.go b/actions/runner_exec.go similarity index 99% rename from pkg/runner/runner_exec.go rename to actions/runner_exec.go index 0a52fab..78b97f5 100644 --- a/pkg/runner/runner_exec.go +++ b/actions/runner_exec.go @@ -10,7 +10,6 @@ import ( "github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/container" - "github.com/nektos/act/pkg/model" log "github.com/sirupsen/logrus" ) @@ -34,7 +33,6 @@ func (runner *runnerImpl) newRunExecutor(run *model.Run) common.Executor { return common.NewPipelineExecutor(executors...) } -/* func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.Executor) (string, error) { var image string logger := newActionLogger(action.Identifier, runner.config.Dryrun) @@ -111,7 +109,6 @@ func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.E return image, nil } -*/ func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors *[]common.Executor) error { logger := newActionLogger(action.Identifier, runner.config.Dryrun) @@ -141,7 +138,11 @@ func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors var cmd, entrypoint []string if action.Args != nil { - cmd = action.Args.Split() + cmd = []string{ + "/bin/sh", + "-c", + action.Args, + } } if action.Runs != nil { entrypoint = action.Runs.Split() diff --git a/cmd/graph.go b/cmd/graph.go index e8d0204..4f8895e 100644 --- a/cmd/graph.go +++ b/cmd/graph.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "os" "github.com/nektos/act/pkg/common" @@ -21,7 +20,7 @@ func drawGraph(plan *model.Plan) error { ids := make([]string, 0) for _, r := range stage.Runs { - ids = append(ids, fmt.Sprintf("%s/%s", r.Workflow.Name, r.JobID)) + ids = append(ids, r.String()) } drawings = append(drawings, jobPen.DrawBoxes(ids...)) } diff --git a/cmd/input.go b/cmd/input.go index 9f6ea5c..0366105 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -7,7 +7,7 @@ import ( // Input contains the input for the root command type Input struct { - workingDir string + workdir string workflowsPath string eventPath string reuseContainers bool @@ -16,7 +16,7 @@ type Input struct { } func (i *Input) resolve(path string) string { - basedir, err := filepath.Abs(i.workingDir) + basedir, err := filepath.Abs(i.workdir) if err != nil { log.Fatal(err) } @@ -29,6 +29,11 @@ func (i *Input) resolve(path string) string { return path } +// Workdir returns path to workdir +func (i *Input) Workdir() string { + return i.resolve(".") +} + // WorkflowsPath returns path to workflows func (i *Input) WorkflowsPath() string { return i.resolve(i.workflowsPath) diff --git a/cmd/root.go b/cmd/root.go index afe9a73..3bde51b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,8 +5,11 @@ import ( "os" "path/filepath" + "github.com/nektos/act/pkg/common" + fswatch "github.com/andreaskoch/go-fswatch" "github.com/nektos/act/pkg/model" + "github.com/nektos/act/pkg/runner" gitignore "github.com/sabhiram/go-gitignore" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -31,7 +34,7 @@ func Execute(ctx context.Context, version string) { rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull docker image(s) if already present") rootCmd.Flags().StringVarP(&input.eventPath, "event", "e", "", "path to event JSON file") rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow files") - rootCmd.PersistentFlags().StringVarP(&input.workingDir, "directory", "C", ".", "working directory") + rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory") rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode") if err := rootCmd.Execute(); err != nil { @@ -87,26 +90,30 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str } // run the plan - // runner, err := runner.New(config) - // if err != nil { - // return err - // } - // defer runner.Close() + config := &runner.Config{ + EventName: eventName, + EventPath: input.EventPath(), + ForcePull: input.forcePull, + ReuseContainers: input.reuseContainers, + Workdir: input.Workdir(), + } + runner, err := runner.New(config) + if err != nil { + return err + } - // if watch, err := cmd.Flags().GetBool("watch"); err != nil { - // return err - // } else if watch { - // return watchAndRun(ctx, func() error { - // return runner.RunPlan(plan) - // }) - // } + ctx = common.WithDryrun(ctx, input.dryrun) + if watch, err := cmd.Flags().GetBool("watch"); err != nil { + return err + } else if watch { + return watchAndRun(ctx, runner.NewPlanExecutor(plan)) + } - // return runner.RunPlan(plan) - return nil + return runner.NewPlanExecutor(plan)(ctx) } } -func watchAndRun(ctx context.Context, fn func() error) error { +func watchAndRun(ctx context.Context, fn common.Executor) error { recurse := true checkIntervalInSeconds := 2 dir, err := os.Getwd() @@ -132,13 +139,13 @@ func watchAndRun(ctx context.Context, fn func() error) error { go func() { for folderWatcher.IsRunning() { - if err = fn(); err != nil { + if err = fn(ctx); err != nil { break } log.Debugf("Watching %s for changes", dir) for changes := range folderWatcher.ChangeDetails() { log.Debugf("%s", changes.String()) - if err = fn(); err != nil { + if err = fn(ctx); err != nil { break } log.Debugf("Watching %s for changes", dir) diff --git a/go.mod b/go.mod index 5aa814e..d135010 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,8 @@ require ( github.com/emirpasic/gods v1.12.0 // indirect github.com/go-ini/ini v1.41.0 github.com/gogo/protobuf v1.2.0 // indirect - github.com/gophercloud/gophercloud v0.7.0 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/gorilla/mux v1.7.0 // indirect - github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93 github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect @@ -24,7 +22,7 @@ require ( github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect - github.com/pkg/errors v0.8.1 // indirect + github.com/pkg/errors v0.8.1 github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 github.com/sirupsen/logrus v1.3.0 github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect @@ -33,9 +31,14 @@ require ( github.com/spf13/pflag v1.0.3 // indirect github.com/stretchr/testify v1.3.0 golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e + golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect + golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect + golang.org/x/text v0.3.2 // indirect golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca // indirect google.golang.org/grpc v1.18.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/ini.v1 v1.41.0 // indirect gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect gopkg.in/src-d/go-git-fixtures.v3 v3.3.0 // indirect diff --git a/go.sum b/go.sum index 30170e1..3bc8655 100644 --- a/go.sum +++ b/go.sum @@ -45,14 +45,10 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/gophercloud/gophercloud v0.7.0 h1:vhmQQEM2SbnGCg2/3EzQnQZ3V7+UCGy9s8exQCprNYg= -github.com/gophercloud/gophercloud v0.7.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93 h1:T1Q6ag9tCwun16AW+XK3tAql24P4uTGUMIn1/92WsQQ= -github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -77,10 +73,6 @@ github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnG github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= @@ -122,8 +114,6 @@ github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc= -golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e h1:egKlR8l7Nu9vHGWbcUV8lqR4987UfUbBd7GbhqGzNYU= golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -133,33 +123,24 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533 h1:bLfqnzrpeG4usq5OvMCrwTdmMJ6aTmlCuo1eKl0mhkI= -golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= @@ -167,9 +148,6 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -197,7 +175,6 @@ gopkg.in/src-d/go-git.v4 v4.9.1 h1:0oKHJZY8tM7B71378cfTg2c5jmWyNlXvestTT6WfY+4= gopkg.in/src-d/go-git.v4 v4.9.1/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/pkg/common/dryrun.go b/pkg/common/dryrun.go new file mode 100644 index 0000000..2d5a14e --- /dev/null +++ b/pkg/common/dryrun.go @@ -0,0 +1,25 @@ +package common + +import ( + "context" +) + +type dryrunContextKey string + +const dryrunContextKeyVal = dryrunContextKey("dryrun") + +// Dryrun returns true if the current context is dryrun +func Dryrun(ctx context.Context) bool { + val := ctx.Value(dryrunContextKeyVal) + if val != nil { + if dryrun, ok := val.(bool); ok { + return dryrun + } + } + return false +} + +// WithDryrun adds a value to the context for dryrun +func WithDryrun(ctx context.Context, dryrun bool) context.Context { + return context.WithValue(ctx, dryrunContextKeyVal, dryrun) +} diff --git a/pkg/common/executor.go b/pkg/common/executor.go index 18c6528..0002567 100644 --- a/pkg/common/executor.go +++ b/pkg/common/executor.go @@ -1,6 +1,7 @@ package common import ( + "context" "fmt" log "github.com/sirupsen/logrus" @@ -25,26 +26,76 @@ func Warningf(format string, args ...interface{}) Warning { } // Executor define contract for the steps of a workflow -type Executor func() error +type Executor func(ctx context.Context) error // Conditional define contract for the conditional predicate -type Conditional func() bool +type Conditional func(ctx context.Context) bool + +// NewInfoExecutor is an executor that logs messages +func NewInfoExecutor(format string, args ...interface{}) Executor { + return func(ctx context.Context) error { + logger := Logger(ctx) + logger.Infof(format, args...) + return nil + } +} // NewPipelineExecutor creates a new executor from a series of other executors func NewPipelineExecutor(executors ...Executor) Executor { - return func() error { - for _, executor := range executors { - if executor == nil { - continue + if executors == nil { + return func(ctx context.Context) error { + return nil + } + } + var rtn Executor + for _, executor := range executors { + if rtn == nil { + rtn = executor + } else { + rtn = rtn.Then(executor) + } + } + return rtn +} + +// NewConditionalExecutor creates a new executor based on conditions +func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor { + return func(ctx context.Context) error { + if conditional(ctx) { + if trueExecutor != nil { + return trueExecutor(ctx) } - err := executor() - if err != nil { - switch err.(type) { - case Warning: - log.Warning(err.Error()) - return nil - default: - log.Debugf("%+v", err) + } else { + if falseExecutor != nil { + return falseExecutor(ctx) + } + } + return nil + } +} + +// NewErrorExecutor creates a new executor that always errors out +func NewErrorExecutor(err error) Executor { + return func(ctx context.Context) error { + return err + } +} + +// NewParallelExecutor creates a new executor from a parallel of other executors +func NewParallelExecutor(executors ...Executor) Executor { + return func(ctx context.Context) error { + errChan := make(chan error) + + for _, executor := range executors { + go executor.ChannelError(errChan)(ctx) + } + + for i := 0; i < len(executors); i++ { + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errChan: + if err != nil { return err } } @@ -53,48 +104,76 @@ func NewPipelineExecutor(executors ...Executor) Executor { } } -// NewConditionalExecutor creates a new executor based on conditions -func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor { - return func() error { - if conditional() { - if trueExecutor != nil { - return trueExecutor() - } - } else { - if falseExecutor != nil { - return falseExecutor() +// ChannelError sends error to errChan rather than returning error +func (e Executor) ChannelError(errChan chan error) Executor { + return func(ctx context.Context) error { + errChan <- e(ctx) + return nil + } +} + +// Then runs another executor if this executor succeeds +func (e Executor) Then(then Executor) Executor { + return func(ctx context.Context) error { + err := e(ctx) + if err != nil { + switch err.(type) { + case Warning: + log.Warning(err.Error()) + default: + log.Debugf("%+v", err) + return err } } + if ctx.Err() != nil { + return ctx.Err() + } + return then(ctx) + } +} + +// If only runs this executor if conditional is true +func (e Executor) If(conditional Conditional) Executor { + return func(ctx context.Context) error { + if conditional(ctx) { + return e(ctx) + } return nil } } -func executeWithChan(executor Executor, errChan chan error) { - errChan <- executor() +// IfNot only runs this executor if conditional is true +func (e Executor) IfNot(conditional Conditional) Executor { + return func(ctx context.Context) error { + if !conditional(ctx) { + return e(ctx) + } + return nil + } } -// NewErrorExecutor creates a new executor that always errors out -func NewErrorExecutor(err error) Executor { - return func() error { +// IfBool only runs this executor if conditional is true +func (e Executor) IfBool(conditional bool) Executor { + return e.If(func(ctx context.Context) bool { + return conditional + }) +} + +// Finally adds an executor to run after other executor +func (e Executor) Finally(finally Executor) Executor { + return func(ctx context.Context) error { + err := e(ctx) + err2 := finally(ctx) + if err2 != nil { + return fmt.Errorf("Error occurred running finally: %v (original error: %v)", err2, err) + } return err } } -// NewParallelExecutor creates a new executor from a parallel of other executors -func NewParallelExecutor(executors ...Executor) Executor { - return func() error { - errChan := make(chan error) - - for _, executor := range executors { - go executeWithChan(executor, errChan) - } - - for i := 0; i < len(executors); i++ { - err := <-errChan - if err != nil { - return err - } - } - return nil +// Not return an inverted conditional +func (c Conditional) Not() Conditional { + return func(ctx context.Context) bool { + return !c(ctx) } } diff --git a/pkg/common/executor_test.go b/pkg/common/executor_test.go index cf85089..215d4fc 100644 --- a/pkg/common/executor_test.go +++ b/pkg/common/executor_test.go @@ -1,6 +1,7 @@ package common import ( + "context" "fmt" "testing" @@ -10,58 +11,62 @@ import ( func TestNewWorkflow(t *testing.T) { assert := assert.New(t) + ctx := context.Background() + // empty emptyWorkflow := NewPipelineExecutor() - assert.Nil(emptyWorkflow()) + assert.Nil(emptyWorkflow(ctx)) // error case errorWorkflow := NewErrorExecutor(fmt.Errorf("test error")) - assert.NotNil(errorWorkflow()) + assert.NotNil(errorWorkflow(ctx)) // multiple success case runcount := 0 successWorkflow := NewPipelineExecutor( - func() error { + func(ctx context.Context) error { runcount++ return nil }, - func() error { + func(ctx context.Context) error { runcount++ return nil }) - assert.Nil(successWorkflow()) + assert.Nil(successWorkflow(ctx)) assert.Equal(2, runcount) } func TestNewConditionalExecutor(t *testing.T) { assert := assert.New(t) + ctx := context.Background() + trueCount := 0 falseCount := 0 - err := NewConditionalExecutor(func() bool { + err := NewConditionalExecutor(func(ctx context.Context) bool { return false - }, func() error { + }, func(ctx context.Context) error { trueCount++ return nil - }, func() error { + }, func(ctx context.Context) error { falseCount++ return nil - })() + })(ctx) assert.Nil(err) assert.Equal(0, trueCount) assert.Equal(1, falseCount) - err = NewConditionalExecutor(func() bool { + err = NewConditionalExecutor(func(ctx context.Context) bool { return true - }, func() error { + }, func(ctx context.Context) error { trueCount++ return nil - }, func() error { + }, func(ctx context.Context) error { falseCount++ return nil - })() + })(ctx) assert.Nil(err) assert.Equal(1, trueCount) @@ -71,13 +76,15 @@ func TestNewConditionalExecutor(t *testing.T) { func TestNewParallelExecutor(t *testing.T) { assert := assert.New(t) + ctx := context.Background() + count := 0 - emptyWorkflow := NewPipelineExecutor(func() error { + emptyWorkflow := NewPipelineExecutor(func(ctx context.Context) error { count++ return nil }) - err := NewParallelExecutor(emptyWorkflow, emptyWorkflow)() + err := NewParallelExecutor(emptyWorkflow, emptyWorkflow)(ctx) assert.Equal(2, count) assert.Nil(err) diff --git a/pkg/common/git.go b/pkg/common/git.go index 03afea2..a3fcb4a 100644 --- a/pkg/common/git.go +++ b/pkg/common/git.go @@ -1,6 +1,7 @@ package common import ( + "context" "errors" "fmt" "io/ioutil" @@ -190,7 +191,7 @@ type NewGitCloneExecutorInput struct { // NewGitCloneExecutor creates an executor to clone git repos func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor { - return func() error { + return func(ctx context.Context) error { input.Logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref) input.Logger.Debugf(" cloning %s to %s", input.URL, input.Dir) diff --git a/pkg/common/logger.go b/pkg/common/logger.go new file mode 100644 index 0000000..a9501ce --- /dev/null +++ b/pkg/common/logger.go @@ -0,0 +1,27 @@ +package common + +import ( + "context" + + "github.com/sirupsen/logrus" +) + +type loggerContextKey string + +const loggerContextKeyVal = loggerContextKey("logrus.FieldLogger") + +// Logger returns the appropriate logger for current context +func Logger(ctx context.Context) logrus.FieldLogger { + val := ctx.Value(loggerContextKeyVal) + if val != nil { + if logger, ok := val.(logrus.FieldLogger); ok { + return logger + } + } + return logrus.StandardLogger() +} + +// WithLogger adds a value to the context for the logger +func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context { + return context.WithValue(ctx, loggerContextKeyVal, logger) +} diff --git a/pkg/container/docker_build.go b/pkg/container/docker_build.go index 9d3c621..d3b5b34 100644 --- a/pkg/container/docker_build.go +++ b/pkg/container/docker_build.go @@ -1,6 +1,7 @@ package container import ( + "context" "io" "os" "path/filepath" @@ -16,16 +17,16 @@ import ( // NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function type NewDockerBuildExecutorInput struct { - DockerExecutorInput ContextDir string ImageTag string } // NewDockerBuildExecutor function to create a run executor for the container func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor { - return func() error { - input.Logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir) - if input.Dryrun { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir) + if common.Dryrun(ctx) { return nil } @@ -33,9 +34,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor { if err != nil { return err } - cli.NegotiateAPIVersion(input.Ctx) + cli.NegotiateAPIVersion(ctx) - input.Logger.Debugf("Building image from '%v'", input.ContextDir) + logger.Debugf("Building image from '%v'", input.ContextDir) tags := []string{input.ImageTag} options := types.ImageBuildOptions{ @@ -49,10 +50,10 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor { defer buildContext.Close() - input.Logger.Debugf("Creating image from context dir '%s' with tag '%s'", input.ContextDir, input.ImageTag) - resp, err := cli.ImageBuild(input.Ctx, buildContext, options) + logger.Debugf("Creating image from context dir '%s' with tag '%s'", input.ContextDir, input.ImageTag) + resp, err := cli.ImageBuild(ctx, buildContext, options) - err = input.logDockerResponse(resp.Body, err != nil) + err = logDockerResponse(logger, resp.Body, err != nil) if err != nil { return err } diff --git a/pkg/container/docker_common.go b/pkg/container/docker_common.go deleted file mode 100644 index 5d9352e..0000000 --- a/pkg/container/docker_common.go +++ /dev/null @@ -1,115 +0,0 @@ -package container - -import ( - "bufio" - "context" - "encoding/json" - "errors" - "fmt" - "github.com/docker/docker/pkg/stdcopy" - "io" - "os" - - "github.com/sirupsen/logrus" -) - -// DockerExecutorInput common input params -type DockerExecutorInput struct { - Ctx context.Context - Logger *logrus.Entry - Dryrun bool -} - -type dockerMessage struct { - ID string `json:"id"` - Stream string `json:"stream"` - Error string `json:"error"` - ErrorDetail struct { - Message string - } - Status string `json:"status"` - Progress string `json:"progress"` -} - -func (i *DockerExecutorInput) logDockerOutput(dockerResponse io.Reader) { - w := i.Logger.Writer() - _, err := stdcopy.StdCopy(w, w, dockerResponse) - if err != nil { - i.Logger.Error(err) - } -} - -func (i *DockerExecutorInput) streamDockerOutput(dockerResponse io.Reader) { - out := os.Stdout - go func() { - <-i.Ctx.Done() - fmt.Println() - }() - - _, err := io.Copy(out, dockerResponse) - if err != nil { - i.Logger.Error(err) - } -} - -func (i *DockerExecutorInput) writeLog(isError bool, format string, args ...interface{}) { - if i.Logger == nil { - return - } - if isError { - i.Logger.Errorf(format, args...) - } else { - i.Logger.Debugf(format, args...) - } - -} - -func (i *DockerExecutorInput) logDockerResponse(dockerResponse io.ReadCloser, isError bool) error { - if dockerResponse == nil { - return nil - } - defer dockerResponse.Close() - - scanner := bufio.NewScanner(dockerResponse) - msg := dockerMessage{} - - for scanner.Scan() { - line := scanner.Bytes() - - msg.ID = "" - msg.Stream = "" - msg.Error = "" - msg.ErrorDetail.Message = "" - msg.Status = "" - msg.Progress = "" - - if err := json.Unmarshal(line, &msg); err != nil { - i.writeLog(false, "Unable to unmarshal line [%s] ==> %v", string(line), err) - continue - } - - if msg.Error != "" { - i.writeLog(isError, "%s", msg.Error) - return errors.New(msg.Error) - } - - if msg.ErrorDetail.Message != "" { - i.writeLog(isError, "%s", msg.ErrorDetail.Message) - return errors.New(msg.Error) - } - - if msg.Status != "" { - if msg.Progress != "" { - i.writeLog(isError, "%s :: %s :: %s\n", msg.Status, msg.ID, msg.Progress) - } else { - i.writeLog(isError, "%s :: %s\n", msg.Status, msg.ID) - } - } else if msg.Stream != "" { - i.writeLog(isError, msg.Stream) - } else { - i.writeLog(false, "Unable to handle line: %s", string(line)) - } - } - - return nil -} diff --git a/pkg/container/docker_logger.go b/pkg/container/docker_logger.go new file mode 100644 index 0000000..8a27d41 --- /dev/null +++ b/pkg/container/docker_logger.go @@ -0,0 +1,117 @@ +package container + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + + "github.com/nektos/act/pkg/common" + "github.com/sirupsen/logrus" + + "github.com/docker/docker/pkg/stdcopy" +) + +type dockerMessage struct { + ID string `json:"id"` + Stream string `json:"stream"` + Error string `json:"error"` + ErrorDetail struct { + Message string + } + Status string `json:"status"` + Progress string `json:"progress"` +} + +func logDockerOutput(ctx context.Context, dockerResponse io.Reader) { + logger := common.Logger(ctx) + if entry, ok := logger.(*logrus.Entry); ok { + w := entry.Writer() + _, err := stdcopy.StdCopy(w, w, dockerResponse) + if err != nil { + logrus.Error(err) + } + } else if lgr, ok := logger.(*logrus.Logger); ok { + w := lgr.Writer() + _, err := stdcopy.StdCopy(w, w, dockerResponse) + if err != nil { + logrus.Error(err) + } + } else { + logrus.Errorf("Unable to get writer from logger (type=%T)", logger) + } +} + +func streamDockerOutput(ctx context.Context, dockerResponse io.Reader) { + out := os.Stdout + go func() { + <-ctx.Done() + fmt.Println() + }() + + _, err := io.Copy(out, dockerResponse) + if err != nil { + logrus.Error(err) + } +} + +func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error { + if dockerResponse == nil { + return nil + } + defer dockerResponse.Close() + + scanner := bufio.NewScanner(dockerResponse) + msg := dockerMessage{} + + for scanner.Scan() { + line := scanner.Bytes() + + msg.ID = "" + msg.Stream = "" + msg.Error = "" + msg.ErrorDetail.Message = "" + msg.Status = "" + msg.Progress = "" + + if err := json.Unmarshal(line, &msg); err != nil { + writeLog(logger, false, "Unable to unmarshal line [%s] ==> %v", string(line), err) + continue + } + + if msg.Error != "" { + writeLog(logger, isError, "%s", msg.Error) + return errors.New(msg.Error) + } + + if msg.ErrorDetail.Message != "" { + writeLog(logger, isError, "%s", msg.ErrorDetail.Message) + return errors.New(msg.Error) + } + + if msg.Status != "" { + if msg.Progress != "" { + writeLog(logger, isError, "%s :: %s :: %s\n", msg.Status, msg.ID, msg.Progress) + } else { + writeLog(logger, isError, "%s :: %s\n", msg.Status, msg.ID) + } + } else if msg.Stream != "" { + writeLog(logger, isError, msg.Stream) + } else { + writeLog(logger, false, "Unable to handle line: %s", string(line)) + } + } + + return nil +} + +func writeLog(logger logrus.FieldLogger, isError bool, format string, args ...interface{}) { + if isError { + logger.Errorf(format, args...) + } else { + logger.Debugf(format, args...) + } +} diff --git a/pkg/container/docker_pull.go b/pkg/container/docker_pull.go index c324598..4e1e5fe 100644 --- a/pkg/container/docker_pull.go +++ b/pkg/container/docker_pull.go @@ -1,40 +1,60 @@ package container import ( + "context" "fmt" "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/nektos/act/pkg/common" + log "github.com/sirupsen/logrus" ) // NewDockerPullExecutorInput the input for the NewDockerPullExecutor function type NewDockerPullExecutorInput struct { - DockerExecutorInput - Image string + Image string + ForcePull bool } // NewDockerPullExecutor function to create a run executor for the container func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor { - return func() error { - input.Logger.Infof("docker pull %v", input.Image) + return func(ctx context.Context) error { + logger := common.Logger(ctx) + logger.Infof("docker pull %v", input.Image) - if input.Dryrun { + if common.Dryrun(ctx) { + return nil + } + + pull := input.ForcePull + if !pull { + imageExists, err := ImageExistsLocally(ctx, input.Image) + log.Debugf("Image exists? %v", imageExists) + if err != nil { + return fmt.Errorf("unable to determine if image already exists for image %q", input.Image) + } + + if !imageExists { + pull = true + } + } + + if !pull { return nil } imageRef := cleanImage(input.Image) - input.Logger.Debugf("pulling image '%v'", imageRef) + logger.Debugf("pulling image '%v'", imageRef) cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return err } - cli.NegotiateAPIVersion(input.Ctx) + cli.NegotiateAPIVersion(ctx) - reader, err := cli.ImagePull(input.Ctx, imageRef, types.ImagePullOptions{}) - _ = input.logDockerResponse(reader, err != nil) + reader, err := cli.ImagePull(ctx, imageRef, types.ImagePullOptions{}) + _ = logDockerResponse(logger, reader, err != nil) if err != nil { return err } diff --git a/pkg/container/docker_run.go b/pkg/container/docker_run.go index 0397ec0..acf7180 100644 --- a/pkg/container/docker_run.go +++ b/pkg/container/docker_run.go @@ -10,12 +10,12 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/nektos/act/pkg/common" + "github.com/pkg/errors" "golang.org/x/crypto/ssh/terminal" ) // NewDockerRunExecutorInput the input for the NewDockerRunExecutor function type NewDockerRunExecutorInput struct { - DockerExecutorInput Image string Entrypoint []string Cmd []string @@ -30,182 +30,198 @@ type NewDockerRunExecutorInput struct { // NewDockerRunExecutor function to create a run executor for the container func NewDockerRunExecutor(input NewDockerRunExecutorInput) common.Executor { - return func() error { + cr := new(containerReference) + cr.input = input - input.Logger.Infof("docker run image=%s entrypoint=%+q cmd=%+q", input.Image, input.Entrypoint, input.Cmd) - if input.Dryrun { + return common. + NewInfoExecutor("docker run image=%s entrypoint=%+q cmd=%+q", input.Image, input.Entrypoint, input.Cmd). + Then( + common.NewPipelineExecutor( + cr.connect(), + cr.find(), + cr.remove().IfBool(!input.ReuseContainers), + cr.create(), + cr.copyContent(), + cr.attach(), + cr.start(), + cr.wait(), + ).Finally( + cr.remove().IfBool(!input.ReuseContainers), + ).IfNot(common.Dryrun), + ) +} + +type containerReference struct { + input NewDockerRunExecutorInput + cli *client.Client + id string +} + +func (cr *containerReference) connect() common.Executor { + return func(ctx context.Context) error { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return errors.WithStack(err) + } + cli.NegotiateAPIVersion(ctx) + cr.cli = cli + return nil + } +} + +func (cr *containerReference) find() common.Executor { + return func(ctx context.Context) error { + containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{ + All: true, + }) + if err != nil { + return errors.WithStack(err) + } + + for _, container := range containers { + for _, name := range container.Names { + if name[1:] == cr.input.Name { + cr.id = container.ID + return nil + } + } + } + + cr.id = "" + return nil + } +} + +func (cr *containerReference) remove() common.Executor { + return func(ctx context.Context) error { + if cr.id == "" { return nil } - cli, err := client.NewClientWithOpts(client.FromEnv) + logger := common.Logger(ctx) + err := cr.cli.ContainerRemove(context.Background(), cr.id, types.ContainerRemoveOptions{ + RemoveVolumes: true, + Force: true, + }) if err != nil { - return err + return errors.WithStack(err) } - cli.NegotiateAPIVersion(input.Ctx) + cr.id = "" - // check if container exists - containerID, err := findContainer(input, cli, input.Name) - if err != nil { - return err - } - - // if we have an old container and we aren't reusing, remove it! - if !input.ReuseContainers && containerID != "" { - input.Logger.Debugf("Found existing container for %s...removing", input.Name) - removeContainer(input, cli, containerID) - containerID = "" - } - - // create a new container if we don't have one to reuse - if containerID == "" { - containerID, err = createContainer(input, cli) - if err != nil { - return err - } - } - - // be sure to cleanup container if we aren't reusing - if !input.ReuseContainers { - defer removeContainer(input, cli, containerID) - } - - executor := common.NewPipelineExecutor( - func() error { - return copyContentToContainer(input, cli, containerID) - }, func() error { - return attachContainer(input, cli, containerID) - }, func() error { - return startContainer(input, cli, containerID) - }, func() error { - return waitContainer(input, cli, containerID) - }, - ) - return executor() - } - -} - -func createContainer(input NewDockerRunExecutorInput, cli *client.Client) (string, error) { - isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) - - config := &container.Config{ - Image: input.Image, - Cmd: input.Cmd, - Entrypoint: input.Entrypoint, - WorkingDir: input.WorkingDir, - Env: input.Env, - Tty: isTerminal, - } - - if len(input.Volumes) > 0 { - config.Volumes = make(map[string]struct{}) - for _, vol := range input.Volumes { - config.Volumes[vol] = struct{}{} - } - } - - resp, err := cli.ContainerCreate(input.Ctx, config, &container.HostConfig{ - Binds: input.Binds, - }, nil, input.Name) - if err != nil { - return "", err - } - input.Logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image) - input.Logger.Debugf("ENV ==> %v", input.Env) - - return resp.ID, nil -} - -func findContainer(input NewDockerRunExecutorInput, cli *client.Client, containerName string) (string, error) { - containers, err := cli.ContainerList(input.Ctx, types.ContainerListOptions{ - All: true, - }) - if err != nil { - return "", err - } - - for _, container := range containers { - for _, name := range container.Names { - if name[1:] == containerName { - return container.ID, nil - } - } - } - - return "", nil -} - -func removeContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) { - err := cli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{ - RemoveVolumes: true, - Force: true, - }) - if err != nil { - input.Logger.Errorf("%v", err) - } - - input.Logger.Debugf("Removed container: %v", containerID) -} - -func copyContentToContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error { - for dstPath, srcReader := range input.Content { - input.Logger.Debugf("Extracting content to '%s'", dstPath) - err := cli.CopyToContainer(input.Ctx, containerID, dstPath, srcReader, types.CopyToContainerOptions{}) - if err != nil { - return err - } - } - return nil -} - -func attachContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error { - out, err := cli.ContainerAttach(input.Ctx, containerID, types.ContainerAttachOptions{ - Stream: true, - Stdout: true, - Stderr: true, - }) - if err != nil { - return err - } - isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) - if !isTerminal || os.Getenv("NORAW") != "" { - go input.logDockerOutput(out.Reader) - } else { - go input.streamDockerOutput(out.Reader) - } - return nil -} - -func startContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error { - input.Logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", input.Image, input.Entrypoint, input.Cmd) - - if err := cli.ContainerStart(input.Ctx, containerID, types.ContainerStartOptions{}); err != nil { - return err - } - - input.Logger.Debugf("Started container: %v", containerID) - return nil -} - -func waitContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error { - statusCh, errCh := cli.ContainerWait(input.Ctx, containerID, container.WaitConditionNotRunning) - var statusCode int64 - select { - case err := <-errCh: - if err != nil { - return err - } - case status := <-statusCh: - statusCode = status.StatusCode - } - - input.Logger.Debugf("Return status: %v", statusCode) - - if statusCode == 0 { + logger.Debugf("Removed container: %v", cr.id) return nil - } else if statusCode == 78 { - return fmt.Errorf("exit with `NEUTRAL`: 78") } - - return fmt.Errorf("exit with `FAILURE`: %v", statusCode) +} + +func (cr *containerReference) create() common.Executor { + return func(ctx context.Context) error { + if cr.id != "" { + return nil + } + logger := common.Logger(ctx) + isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) + + input := cr.input + config := &container.Config{ + Image: input.Image, + Cmd: input.Cmd, + Entrypoint: input.Entrypoint, + WorkingDir: input.WorkingDir, + Env: input.Env, + Tty: isTerminal, + } + + if len(input.Volumes) > 0 { + config.Volumes = make(map[string]struct{}) + for _, vol := range input.Volumes { + config.Volumes[vol] = struct{}{} + } + } + + resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{ + Binds: input.Binds, + }, nil, input.Name) + if err != nil { + return errors.WithStack(err) + } + logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image) + logger.Debugf("ENV ==> %v", input.Env) + + cr.id = resp.ID + return nil + } +} + +func (cr *containerReference) copyContent() common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + for dstPath, srcReader := range cr.input.Content { + logger.Debugf("Extracting content to '%s'", dstPath) + err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, srcReader, types.CopyToContainerOptions{}) + if err != nil { + return errors.WithStack(err) + } + } + return nil + } +} + +func (cr *containerReference) attach() common.Executor { + return func(ctx context.Context) error { + out, err := cr.cli.ContainerAttach(ctx, cr.id, types.ContainerAttachOptions{ + Stream: true, + Stdout: true, + Stderr: true, + }) + if err != nil { + return errors.WithStack(err) + } + isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) + if !isTerminal || os.Getenv("NORAW") != "" { + go logDockerOutput(ctx, out.Reader) + } else { + go streamDockerOutput(ctx, out.Reader) + } + return nil + } +} + +func (cr *containerReference) start() common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", cr.input.Image, cr.input.Entrypoint, cr.input.Cmd) + + if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil { + return errors.WithStack(err) + } + + logger.Debugf("Started container: %v", cr.id) + return nil + } +} + +func (cr *containerReference) wait() common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + statusCh, errCh := cr.cli.ContainerWait(ctx, cr.id, container.WaitConditionNotRunning) + var statusCode int64 + select { + case err := <-errCh: + if err != nil { + return errors.WithStack(err) + } + case status := <-statusCh: + statusCode = status.StatusCode + } + + logger.Debugf("Return status: %v", statusCode) + + if statusCode == 0 { + return nil + } else if statusCode == 78 { + return fmt.Errorf("exit with `NEUTRAL`: 78") + } + + return fmt.Errorf("exit with `FAILURE`: %v", statusCode) + } } diff --git a/pkg/container/docker_run_test.go b/pkg/container/docker_run_test.go index 9d92b22..54575c6 100644 --- a/pkg/container/docker_run_test.go +++ b/pkg/container/docker_run_test.go @@ -30,27 +30,21 @@ func TestNewDockerRunExecutor(t *testing.T) { logger.SetOutput(buf) logger.SetFormatter(&rawFormatter{}) + ctx := common.WithLogger(context.Background(), logger) + runner := NewDockerRunExecutor(NewDockerRunExecutorInput{ - DockerExecutorInput: DockerExecutorInput{ - Ctx: context.TODO(), - Logger: logrus.NewEntry(logger), - }, Image: "hello-world", }) puller := NewDockerPullExecutor(NewDockerPullExecutorInput{ - DockerExecutorInput: DockerExecutorInput{ - Ctx: context.TODO(), - Logger: logrus.NewEntry(noopLogger), - }, Image: "hello-world", }) pipeline := common.NewPipelineExecutor(puller, runner) - err := pipeline() + err := pipeline(ctx) assert.NoError(t, err) - expected := `docker run image=hello-world entrypoint=[] cmd=[]Hello from Docker!` + expected := `docker pull hello-worlddocker run image=hello-world entrypoint=[] cmd=[]Hello from Docker!` actual := buf.String() assert.Equal(t, expected, actual[:len(expected)]) } diff --git a/pkg/model/planner.go b/pkg/model/planner.go index 854bf81..47d1ee4 100644 --- a/pkg/model/planner.go +++ b/pkg/model/planner.go @@ -1,6 +1,7 @@ package model import ( + "fmt" "io/ioutil" "math" "os" @@ -33,6 +34,19 @@ type Run struct { JobID string } +func (r *Run) String() string { + jobName := r.Job().Name + if jobName == "" { + jobName = r.JobID + } + return fmt.Sprintf("%s/%s", r.Workflow.Name, jobName) +} + +// Job returns the job for this Run +func (r *Run) Job() *Job { + return r.Workflow.GetJob(r.JobID) +} + // NewWorkflowPlanner will load all workflows from a directory func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) { log.Debugf("Loading workflows from '%s'", dirname) @@ -55,6 +69,9 @@ func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) { f.Close() return nil, err } + if workflow.Name == "" { + workflow.Name = file.Name() + } wp.workflows = append(wp.workflows, workflow) f.Close() } diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 9be0dfa..616867b 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -1,7 +1,9 @@ package model import ( + "fmt" "io" + "strings" "gopkg.in/yaml.v2" ) @@ -16,13 +18,26 @@ type Workflow struct { // Job is the structure of one job in a workflow type Job struct { - Name string `yaml:"name"` - Needs []string `yaml:"needs"` - RunsOn string `yaml:"runs-on"` - Env map[string]string `yaml:"env"` - If string `yaml:"if"` - Steps []*Step `yaml:"steps"` - TimeoutMinutes int64 `yaml:"timeout-minutes"` + Name string `yaml:"name"` + Needs []string `yaml:"needs"` + RunsOn string `yaml:"runs-on"` + Env map[string]string `yaml:"env"` + If string `yaml:"if"` + Steps []*Step `yaml:"steps"` + TimeoutMinutes int64 `yaml:"timeout-minutes"` + Container *ContainerSpec `yaml:"container"` + Services map[string]*ContainerSpec `yaml:"services"` +} + +// ContainerSpec is the specification of the container to use for the job +type ContainerSpec struct { + Image string `yaml:"image"` + Env map[string]string `yaml:"env"` + Ports []int `yaml:"ports"` + Volumes []string `yaml:"volumes"` + Options string `yaml:"options"` + Entrypoint string + Args string } // Step is the structure of one step in a job @@ -40,6 +55,19 @@ type Step struct { TimeoutMinutes int64 `yaml:"timeout-minutes"` } +// GetEnv gets the env for a step +func (s *Step) GetEnv() map[string]string { + rtnEnv := make(map[string]string) + for k, v := range s.Env { + rtnEnv[k] = v + } + for k, v := range s.With { + envKey := fmt.Sprintf("INPUT_%s", strings.ToUpper(k)) + rtnEnv[envKey] = v + } + return rtnEnv +} + // ReadWorkflow returns a list of jobs for a given workflow file reader func ReadWorkflow(in io.Reader) (*Workflow, error) { w := new(Workflow) diff --git a/actions/log.go b/pkg/runner/logger.go similarity index 60% rename from actions/log.go rename to pkg/runner/logger.go index 7a8fef2..b36b9b9 100644 --- a/actions/log.go +++ b/pkg/runner/logger.go @@ -1,4 +1,4 @@ -package actions +package runner import ( "bytes" @@ -11,15 +11,6 @@ import ( "golang.org/x/crypto/ssh/terminal" ) -type actionLogFormatter struct { -} - -var formatter *actionLogFormatter - -func init() { - formatter = new(actionLogFormatter) -} - const ( //nocolor = 0 red = 31 @@ -29,16 +20,20 @@ const ( gray = 37 ) -func newActionLogger(actionName string, dryrun bool) *logrus.Entry { +// NewJobLogger gets the logger for the Job +func NewJobLogger(jobName string, dryrun bool) logrus.FieldLogger { logger := logrus.New() - logger.SetFormatter(formatter) + logger.SetFormatter(new(jobLogFormatter)) logger.SetOutput(os.Stdout) logger.SetLevel(logrus.GetLevel()) - rtn := logger.WithFields(logrus.Fields{"action_name": actionName, "dryrun": dryrun}) + rtn := logger.WithFields(logrus.Fields{"job_name": jobName, "dryrun": dryrun}) return rtn } -func (f *actionLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { +type jobLogFormatter struct { +} + +func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { b := &bytes.Buffer{} if f.isColored(entry) { @@ -51,7 +46,7 @@ func (f *actionLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { return b.Bytes(), nil } -func (f *actionLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) { +func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) { var levelColor int switch entry.Level { case logrus.DebugLevel, logrus.TraceLevel: @@ -65,27 +60,27 @@ func (f *actionLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) } entry.Message = strings.TrimSuffix(entry.Message, "\n") - actionName := entry.Data["action_name"] + jobName := entry.Data["job_name"] if entry.Data["dryrun"] == true { - fmt.Fprintf(b, "\x1b[%dm*DRYRUN* \x1b[%dm[%s] \x1b[0m%s", green, levelColor, actionName, entry.Message) + fmt.Fprintf(b, "\x1b[%dm*DRYRUN* \x1b[%dm[%s] \x1b[0m%s", green, levelColor, jobName, entry.Message) } else { - fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s", levelColor, actionName, entry.Message) + fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s", levelColor, jobName, entry.Message) } } -func (f *actionLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) { +func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) { entry.Message = strings.TrimSuffix(entry.Message, "\n") - actionName := entry.Data["action_name"] + jobName := entry.Data["job_name"] if entry.Data["dryrun"] == true { - fmt.Fprintf(b, "*DRYRUN* [%s] %s", actionName, entry.Message) + fmt.Fprintf(b, "*DRYRUN* [%s] %s", jobName, entry.Message) } else { - fmt.Fprintf(b, "[%s] %s", actionName, entry.Message) + fmt.Fprintf(b, "[%s] %s", jobName, entry.Message) } } -func (f *actionLogFormatter) isColored(entry *logrus.Entry) bool { +func (f *jobLogFormatter) isColored(entry *logrus.Entry) bool { isColored := checkIfTerminal(entry.Logger.Out) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go new file mode 100644 index 0000000..e00c9c4 --- /dev/null +++ b/pkg/runner/run_context.go @@ -0,0 +1,269 @@ +package runner + +import ( + "archive/tar" + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/nektos/act/pkg/container" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/model" + log "github.com/sirupsen/logrus" +) + +// RunContext contains info about current job +type RunContext struct { + Config *Config + Run *model.Run + EventJSON string + Env map[string]string + Outputs map[string]string + Tempdir string +} + +// GetEnv returns the env for the context +func (rc *RunContext) GetEnv() map[string]string { + if rc.Env == nil { + rc.Env = mergeMaps(rc.Run.Workflow.Env, rc.Run.Job().Env) + } + return rc.Env +} + +// StepEnv returns the env for a step +func (rc *RunContext) StepEnv(step *model.Step) map[string]string { + env := make(map[string]string) + env["HOME"] = "/github/home" + env["GITHUB_WORKFLOW"] = rc.Run.Workflow.Name + env["GITHUB_RUN_ID"] = "1" + env["GITHUB_RUN_NUMBER"] = "1" + env["GITHUB_ACTION"] = step.ID + env["GITHUB_ACTOR"] = "nektos/act" + + repoPath := rc.Config.Workdir + repo, err := common.FindGithubRepo(repoPath) + if err != nil { + log.Warningf("unable to get git repo: %v", err) + } else { + env["GITHUB_REPOSITORY"] = repo + } + env["GITHUB_EVENT_NAME"] = rc.Config.EventName + env["GITHUB_EVENT_PATH"] = "/github/workflow/event.json" + env["GITHUB_WORKSPACE"] = "/github/workspace" + + _, rev, err := common.FindGitRevision(repoPath) + if err != nil { + log.Warningf("unable to get git revision: %v", err) + } else { + env["GITHUB_SHA"] = rev + } + + ref, err := common.FindGitRef(repoPath) + if err != nil { + log.Warningf("unable to get git ref: %v", err) + } else { + log.Infof("using github ref: %s", ref) + env["GITHUB_REF"] = ref + } + job := rc.Run.Job() + if job.Container != nil { + return mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv(), env) + } + return mergeMaps(rc.GetEnv(), step.GetEnv(), env) +} + +// Close cleans up temp dir +func (rc *RunContext) Close(ctx context.Context) error { + return os.RemoveAll(rc.Tempdir) +} + +// Executor returns a pipeline executor for all the steps in the job +func (rc *RunContext) Executor() common.Executor { + steps := make([]common.Executor, 0) + steps = append(steps, rc.setupTempDir()) + + for _, step := range rc.Run.Job().Steps { + containerSpec := new(model.ContainerSpec) + + var stepExecutor common.Executor + if step.Run != "" { + stepExecutor = common.NewPipelineExecutor( + rc.setupContainerSpec(step, containerSpec), + rc.pullImage(containerSpec), + rc.runContainer(containerSpec), + ) + } else if step.Uses != "" { + stepExecutor = common.NewErrorExecutor(fmt.Errorf("Not yet implemented - job:%s step:%+v", rc.Run, step)) + // clone action repo + // read action.yaml + // if runs.using == node12, start node12 container and run `main` + // if runs.using == docker, pull `image` and run + // set inputs as env + // caputre output + } else { + stepExecutor = common.NewErrorExecutor(fmt.Errorf("Unable to determine how to run job:%s step:%+v", rc.Run, step)) + } + steps = append(steps, stepExecutor) + } + return common.NewPipelineExecutor(steps...).Finally(rc.Close) +} + +func (rc *RunContext) setupContainerSpec(step *model.Step, containerSpec *model.ContainerSpec) common.Executor { + return func(ctx context.Context) error { + job := rc.Run.Job() + + containerSpec.Env = rc.StepEnv(step) + + if step.Uses != "" { + containerSpec.Image = step.Uses + } else if job.Container != nil { + containerSpec.Image = job.Container.Image + containerSpec.Args = step.Run + containerSpec.Ports = job.Container.Ports + containerSpec.Volumes = job.Container.Volumes + containerSpec.Options = job.Container.Options + } else if step.Run != "" { + containerSpec.Image = platformImage(job.RunsOn) + containerSpec.Args = step.Run + } else { + return fmt.Errorf("Unable to setup container for %s", step) + } + return nil + } +} + +func platformImage(platform string) string { + switch platform { + case "ubuntu-latest", "ubuntu-18.04": + return "ubuntu:18.04" + case "ubuntu-16.04": + return "ubuntu:16.04" + case "windows-latest", "windows-2019", "macos-latest", "macos-10.15": + return "" + default: + return "" + } +} + +func mergeMaps(maps ...map[string]string) map[string]string { + rtnMap := make(map[string]string) + for _, m := range maps { + for k, v := range m { + rtnMap[k] = v + } + } + return rtnMap +} + +func (rc *RunContext) setupTempDir() common.Executor { + return func(ctx context.Context) error { + var err error + rc.Tempdir, err = ioutil.TempDir("", "act-") + return err + } +} + +func (rc *RunContext) pullImage(containerSpec *model.ContainerSpec) common.Executor { + return func(ctx context.Context) error { + return container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{ + Image: containerSpec.Image, + ForcePull: rc.Config.ForcePull, + })(ctx) + } +} + +func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Executor { + return func(ctx context.Context) error { + ghReader, err := rc.createGithubTarball() + if err != nil { + return err + } + + envList := make([]string, 0) + for k, v := range containerSpec.Env { + envList = append(envList, fmt.Sprintf("%s=%s", k, v)) + } + var cmd, entrypoint []string + if containerSpec.Args != "" { + cmd = []string{ + "/bin/sh", + "-c", + containerSpec.Args, + } + } + if containerSpec.Entrypoint != "" { + entrypoint = strings.Fields(containerSpec.Entrypoint) + } + + return container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{ + Cmd: cmd, + Entrypoint: entrypoint, + Image: containerSpec.Image, + WorkingDir: "/github/workspace", + Env: envList, + Name: rc.createContainerName(), + Binds: []string{ + fmt.Sprintf("%s:%s", rc.Config.Workdir, "/github/workspace"), + fmt.Sprintf("%s:%s", rc.Tempdir, "/github/home"), + fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"), + }, + Content: map[string]io.Reader{"/github": ghReader}, + ReuseContainers: rc.Config.ReuseContainers, + })(ctx) + } +} + +func (rc *RunContext) createGithubTarball() (io.Reader, error) { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + var files = []struct { + Name string + Mode int64 + Body string + }{ + {"workflow/event.json", 0644, rc.EventJSON}, + } + for _, file := range files { + log.Debugf("Writing entry to tarball %s len:%d", file.Name, len(rc.EventJSON)) + hdr := &tar.Header{ + Name: file.Name, + Mode: file.Mode, + Size: int64(len(rc.EventJSON)), + } + if err := tw.WriteHeader(hdr); err != nil { + return nil, err + } + if _, err := tw.Write([]byte(rc.EventJSON)); err != nil { + return nil, err + } + } + if err := tw.Close(); err != nil { + return nil, err + } + + return &buf, nil + +} + +func (rc *RunContext) createContainerName() string { + containerName := regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(rc.Run.String(), "-") + + prefix := fmt.Sprintf("%s-", trimToLen(filepath.Base(rc.Config.Workdir), 10)) + suffix := "" + containerName = trimToLen(containerName, 30-(len(prefix)+len(suffix))) + return fmt.Sprintf("%s%s%s", prefix, containerName, suffix) +} + +func trimToLen(s string, l int) string { + if len(s) > l { + return s[:l] + } + return s +} diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index c242e67..45f60be 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -1,9 +1,7 @@ package runner import ( - "io" "io/ioutil" - "os" "github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/model" @@ -12,18 +10,13 @@ import ( // Runner provides capabilities to run GitHub actions type Runner interface { - PlanRunner - io.Closer -} - -// PlanRunner to run a specific actions -type PlanRunner interface { - RunPlan(plan *model.Plan) error + NewPlanExecutor(plan *model.Plan) common.Executor + NewRunExecutor(run *model.Run) common.Executor } // Config contains the config for a new runner type Config struct { - Dryrun bool // don't start any of the containers + Workdir string // path to working directory EventName string // name of event to run EventPath string // path to JSON file to use for event.json in containers ReuseContainers bool // reuse containers to maintain state @@ -32,57 +25,44 @@ type Config struct { type runnerImpl struct { config *Config - tempDir string eventJSON string } -// NewRunner Creates a new Runner -func NewRunner(runnerConfig *Config) (Runner, error) { +// New Creates a new Runner +func New(runnerConfig *Config) (Runner, error) { runner := &runnerImpl{ config: runnerConfig, } - init := common.NewPipelineExecutor( - runner.setupTempDir, - runner.setupEvent, - ) - - return runner, init() -} - -func (runner *runnerImpl) setupTempDir() error { - var err error - runner.tempDir, err = ioutil.TempDir("", "act-") - return err -} - -func (runner *runnerImpl) setupEvent() error { runner.eventJSON = "{}" - if runner.config.EventPath != "" { + if runnerConfig.EventPath != "" { log.Debugf("Reading event.json from %s", runner.config.EventPath) eventJSONBytes, err := ioutil.ReadFile(runner.config.EventPath) if err != nil { - return err + return nil, err } runner.eventJSON = string(eventJSONBytes) } - return nil + return runner, nil } -func (runner *runnerImpl) RunPlan(plan *model.Plan) error { +func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { pipeline := make([]common.Executor, 0) for _, stage := range plan.Stages { stageExecutor := make([]common.Executor, 0) for _, run := range stage.Runs { - stageExecutor = append(stageExecutor, runner.newRunExecutor(run)) + stageExecutor = append(stageExecutor, runner.NewRunExecutor(run)) } pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...)) } - executor := common.NewPipelineExecutor(pipeline...) - return executor() + return common.NewPipelineExecutor(pipeline...) } -func (runner *runnerImpl) Close() error { - return os.RemoveAll(runner.tempDir) +func (runner *runnerImpl) NewRunExecutor(run *model.Run) common.Executor { + rc := new(RunContext) + rc.Config = runner.config + rc.Run = run + rc.EventJSON = runner.eventJSON + return rc.Executor() }