successfully able to run simple workflows
Signed-off-by: Casey Lee <cplee@nektos.com>
This commit is contained in:
parent
8c49ba0cec
commit
532af98aef
23 changed files with 958 additions and 495 deletions
12
.github/workflows/basic.yml
vendored
12
.github/workflows/basic.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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...))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
43
cmd/root.go
43
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)
|
||||
|
|
9
go.mod
9
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
|
||||
|
|
25
go.sum
25
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=
|
||||
|
|
25
pkg/common/dryrun.go
Normal file
25
pkg/common/dryrun.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
27
pkg/common/logger.go
Normal file
27
pkg/common/logger.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
117
pkg/container/docker_logger.go
Normal file
117
pkg/container/docker_logger.go
Normal file
|
@ -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...)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)])
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
269
pkg/runner/run_context.go
Normal file
269
pkg/runner/run_context.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue