Add option to run custom architecture (container platform) (#525)

* Add QEMU to run different architectures

* Update dependencies in `go.mod`

* Add `--container-architecture` flag to specify custom image architecture

Co-authored-by: Casey Lee <cplee@nektos.com>
This commit is contained in:
hackercat 2021-03-29 06:08:40 +02:00 committed by GitHub
parent 41b03b581c
commit 6c258cf40d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1254 additions and 164 deletions

View file

@ -18,6 +18,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- uses: actions/setup-go@v1
with:
go-version: 1.14

View file

@ -85,31 +85,32 @@ It will save that information to `~/.actrc`, please refer to [Configuration](#co
# Flags
```none
-a, --actor string user that triggered the event (default "nektos/act")
-b, --bind bind working directory to container, rather than copy
--defaultbranch string the name of the main branch
-C, --directory string working directory (default ".")
-n, --dryrun dryrun mode
--env-file string environment file to read and use as env in the containers (default ".env")
--detect-event Use first event type from workflow as event that triggered the workflow
-e, --eventpath string path to event JSON file
-g, --graph draw workflows
-h, --help help for act
--insecure-secrets NOT RECOMMENDED! Doesn't hide secrets while printing logs.
-j, --job string run job
-l, --list list workflows
-P, --platform stringArray custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)
--privileged use privileged mode
-p, --pull pull docker image(s) if already present
-q, --quiet disable logging of output from steps
-r, --reuse reuse action containers to maintain state
-s, --secret stringArray secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)
--secret-file string file with list of secrets to read from (e.g. --secret-file .secrets) (default ".secrets")
--userns string user namespace to use
-v, --verbose verbose output
--version version for act
-w, --watch watch the contents of the local repo and run when files change
-W, --workflows string path to workflow file(s) (default "./.github/workflows/")
-a, --actor string user that triggered the event (default "nektos/act")
-b, --bind bind working directory to container, rather than copy
--container-architecture string Architecture which should be used to run containers, e.g.: linux/amd64. Defaults to linux/<your machine architecture> [linux/amd64]
--defaultbranch string the name of the main branch
--detect-event Use first event type from workflow as event that triggered the workflow
-C, --directory string working directory (default ".")
-n, --dryrun dryrun mode
--env stringArray env to make available to actions with optional value (e.g. --e myenv=foo or -s myenv)
--env-file string environment file to read and use as env in the containers (default ".env")
-e, --eventpath string path to event JSON file
-g, --graph draw workflows
-h, --help help for act
--insecure-secrets NOT RECOMMENDED! Doesn't hide secrets while printing logs.
-j, --job string run job
-l, --list list workflows
-P, --platform stringArray custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)
--privileged use privileged mode
-p, --pull pull docker image(s) if already present
-q, --quiet disable logging of output from steps
-r, --reuse reuse action containers to maintain state
-s, --secret stringArray secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)
--secret-file string file with list of secrets to read from (e.g. --secret-file .secrets) (default ".secrets")
--userns string user namespace to use
-v, --verbose verbose output
-w, --watch watch the contents of the local repo and run when files change
-W, --workflows string path to workflow file(s) (default "./.github/workflows/")
```
# Known Issues

View file

@ -7,25 +7,26 @@ import (
// Input contains the input for the root command
type Input struct {
actor string
workdir string
workflowsPath string
autodetectEvent bool
eventPath string
reuseContainers bool
bindWorkdir bool
secrets []string
envs []string
platforms []string
dryrun bool
forcePull bool
noOutput bool
envfile string
secretfile string
insecureSecrets bool
defaultBranch string
privileged bool
usernsMode string
actor string
workdir string
workflowsPath string
autodetectEvent bool
eventPath string
reuseContainers bool
bindWorkdir bool
secrets []string
envs []string
platforms []string
dryrun bool
forcePull bool
noOutput bool
envfile string
secretfile string
insecureSecrets bool
defaultBranch string
privileged bool
usernsMode string
containerArchitecture string
}
func (i *Input) resolve(path string) string {

View file

@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/nektos/act/pkg/common"
@ -57,6 +58,7 @@ func Execute(ctx context.Context, version string) {
rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. Defaults to linux/<your machine architecture> [linux/"+runtime.GOARCH+"]")
rootCmd.SetArgs(args())
if err := rootCmd.Execute(); err != nil {
@ -247,21 +249,22 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
// run the plan
config := &runner.Config{
Actor: input.actor,
EventName: eventName,
EventPath: input.EventPath(),
DefaultBranch: defaultbranch,
ForcePull: input.forcePull,
ReuseContainers: input.reuseContainers,
Workdir: input.Workdir(),
BindWorkdir: input.bindWorkdir,
LogOutput: !input.noOutput,
Env: envs,
Secrets: secrets,
InsecureSecrets: input.insecureSecrets,
Platforms: input.newPlatforms(),
Privileged: input.privileged,
UsernsMode: input.usernsMode,
Actor: input.actor,
EventName: eventName,
EventPath: input.EventPath(),
DefaultBranch: defaultbranch,
ForcePull: input.forcePull,
ReuseContainers: input.reuseContainers,
Workdir: input.Workdir(),
BindWorkdir: input.bindWorkdir,
LogOutput: !input.noOutput,
Env: envs,
Secrets: secrets,
InsecureSecrets: input.insecureSecrets,
Platforms: input.newPlatforms(),
Privileged: input.privileged,
UsernsMode: input.usernsMode,
ContainerArchitecture: input.containerArchitecture,
}
r, err := runner.New(config)
if err != nil {

22
go.mod
View file

@ -4,44 +4,36 @@ go 1.14
require (
github.com/AlecAivazis/survey/v2 v2.2.7
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/MichaelTJones/walk v0.0.0-20161122175330-4748e29d5718 // indirect
github.com/Microsoft/go-winio v0.4.15 // indirect
github.com/Microsoft/hcsshim v0.8.10 // indirect
github.com/andreaskoch/go-fswatch v1.0.0
github.com/containerd/containerd v1.4.1 // indirect
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a // indirect
github.com/docker/cli v20.10.0-rc1+incompatible
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/cli v20.10.3+incompatible
github.com/docker/docker v20.10.3+incompatible
github.com/go-git/go-billy/v5 v5.0.0
github.com/go-git/go-git/v5 v5.2.0
github.com/go-ini/ini v1.62.0
github.com/golang/protobuf v1.4.3 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/mux v1.7.0 // indirect
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
github.com/imdario/mergo v0.3.11 // indirect
github.com/joho/godotenv v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/mgutz/str v1.2.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v0.1.1 // indirect
github.com/moby/buildkit v0.8.1
github.com/moby/sys/mount v0.2.0 // indirect
github.com/opencontainers/image-spec v1.0.1
github.com/pkg/errors v0.9.1
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
github.com/sirupsen/logrus v1.7.0
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/spf13/cobra v1.1.1
github.com/stretchr/testify v1.7.0
github.com/xanzy/ssh-agent v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221
google.golang.org/genproto v0.0.0-20201117123952-62d171c70ae1 // indirect
google.golang.org/grpc v1.33.2 // indirect
@ -50,5 +42,3 @@ require (
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
gotest.tools/v3 v3.0.2
)
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a

955
go.sum

File diff suppressed because it is too large Load diff

View file

@ -7,24 +7,28 @@ import (
"path/filepath"
"github.com/docker/docker/api/types"
"github.com/docker/docker/builder/dockerignore"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
"github.com/nektos/act/pkg/common"
// github.com/docker/docker/builder/dockerignore is deprecated
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
)
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
type NewDockerBuildExecutorInput struct {
ContextDir string
ImageTag string
Platform string
}
// NewDockerBuildExecutor function to create a run executor for the container
func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
logger.Infof("%sdocker build -t %s %s", logPrefix, input.ImageTag, input.ContextDir)
logger.Infof("%sdocker build -t %s --platform %s %s", logPrefix, input.ImageTag, input.Platform, input.ContextDir)
if common.Dryrun(ctx) {
return nil
}
@ -38,8 +42,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
tags := []string{input.ImageTag}
options := types.ImageBuildOptions{
Tags: tags,
Remove: true,
Tags: tags,
Remove: true,
Platform: input.Platform,
}
buildContext, err := createBuildContext(input.ContextDir, "Dockerfile")
@ -49,7 +54,7 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
defer buildContext.Close()
logger.Debugf("Creating image from context dir '%s' with tag '%s'", input.ContextDir, input.ImageTag)
logger.Debugf("Creating image from context dir '%s' with tag '%s' and platform '%s'", input.ContextDir, input.ImageTag, input.Platform)
resp, err := cli.ImageBuild(ctx, buildContext, options)
err = logDockerResponse(logger, resp.Body, err != nil)

View file

@ -2,14 +2,15 @@ package container
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
)
// ImageExistsLocally returns a boolean indicating if an image with the
// requested name (and tag) exist in the local docker image store
func ImageExistsLocally(ctx context.Context, imageName string) (bool, error) {
// requested name, tag and architecture exists in the local docker image store
func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) {
cli, err := GetDockerClient(ctx)
if err != nil {
return false, err
@ -27,5 +28,61 @@ func ImageExistsLocally(ctx context.Context, imageName string) (bool, error) {
return false, err
}
return len(images) > 0, nil
if len(images) > 0 {
if platform == "any" {
return true, nil
}
for _, v := range images {
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, v.ID)
if err != nil {
return false, err
}
if fmt.Sprintf("%s/%s", inspectImage.Os, inspectImage.Architecture) == platform {
return true, nil
}
}
return false, nil
}
return false, nil
}
// DeleteImage removes image from local store, the function is used to run different
// container image architectures
func DeleteImage(ctx context.Context, imageName string) (bool, error) {
if exists, err := ImageExistsLocally(ctx, imageName, "any"); !exists {
return false, err
}
cli, err := GetDockerClient(ctx)
if err != nil {
return false, err
}
filters := filters.NewArgs()
filters.Add("reference", imageName)
imageListOptions := types.ImageListOptions{
Filters: filters,
}
images, err := cli.ImageList(ctx, imageListOptions)
if err != nil {
return false, err
}
if len(images) > 0 {
for _, v := range images {
if _, err = cli.ImageRemove(ctx, v.ID, types.ImageRemoveOptions{
Force: true,
PruneChildren: true,
}); err != nil {
return false, err
}
}
return true, nil
}
return false, nil
}

View file

@ -23,9 +23,15 @@ func TestImageExistsLocally(t *testing.T) {
// to help make this test reliable and not flaky, we need to have
// an image that will exist, and onew that won't exist
exists, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist")
// Test if image exists with specific tag
invalidImageTag, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
assert.Nil(t, err)
assert.Equal(t, false, exists)
assert.Equal(t, false, invalidImageTag)
// Test if image exists with specific architecture (image platform)
invalidImagePlatform, err := ImageExistsLocally(ctx, "alpine:latest", "windows/amd64")
assert.Nil(t, err)
assert.Equal(t, false, invalidImagePlatform)
// pull an image
cli, err := client.NewClientWithOpts(client.FromEnv)
@ -34,13 +40,28 @@ func TestImageExistsLocally(t *testing.T) {
// Chose alpine latest because it's so small
// maybe we should build an image instead so that tests aren't reliable on dockerhub
reader, err := cli.ImagePull(ctx, "alpine:latest", types.ImagePullOptions{})
readerDefault, err := cli.ImagePull(ctx, "alpine:latest", types.ImagePullOptions{
Platform: "linux/amd64",
})
assert.Nil(t, err)
defer reader.Close()
_, err = ioutil.ReadAll(reader)
defer readerDefault.Close()
_, err = ioutil.ReadAll(readerDefault)
assert.Nil(t, err)
exists, err = ImageExistsLocally(ctx, "alpine:latest")
imageDefaultArchExists, err := ImageExistsLocally(ctx, "alpine:latest", "linux/amd64")
assert.Nil(t, err)
assert.Equal(t, true, exists)
assert.Equal(t, true, imageDefaultArchExists)
// Validate if another architecture platform can be pulled
readerArm64, err := cli.ImagePull(ctx, "alpine:latest", types.ImagePullOptions{
Platform: "linux/arm64",
})
assert.Nil(t, err)
defer readerArm64.Close()
_, err = ioutil.ReadAll(readerArm64)
assert.Nil(t, err)
imageArm64Exists, err := ImageExistsLocally(ctx, "alpine:latest", "linux/arm64")
assert.Nil(t, err)
assert.Equal(t, true, imageArm64Exists)
}

View file

@ -6,15 +6,17 @@ import (
"strings"
"github.com/docker/docker/api/types"
"github.com/nektos/act/pkg/common"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
)
// NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
type NewDockerPullExecutorInput struct {
Image string
ForcePull bool
Platform string
}
// NewDockerPullExecutor function to create a run executor for the container
@ -29,10 +31,10 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
pull := input.ForcePull
if !pull {
imageExists, err := ImageExistsLocally(ctx, input.Image)
imageExists, err := ImageExistsLocally(ctx, input.Image, input.Platform)
log.Debugf("Image exists? %v", imageExists)
if err != nil {
return errors.WithMessagef(err, "unable to determine if image already exists for image %q", input.Image)
return errors.WithMessagef(err, "unable to determine if image already exists for image %q (%s)", input.Image, input.Platform)
}
if !imageExists {
@ -45,14 +47,16 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
}
imageRef := cleanImage(input.Image)
logger.Debugf("pulling image '%v'", imageRef)
logger.Debugf("pulling image '%v' (%s)", imageRef, input.Platform)
cli, err := GetDockerClient(ctx)
if err != nil {
return err
}
reader, err := cli.ImagePull(ctx, imageRef, types.ImagePullOptions{})
reader, err := cli.ImagePull(ctx, imageRef, types.ImagePullOptions{
Platform: input.Platform,
})
_ = logDockerResponse(logger, reader, err != nil)
if err != nil {
return err

View file

@ -24,10 +24,13 @@ import (
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/nektos/act/pkg/common"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"golang.org/x/term"
"github.com/nektos/act/pkg/common"
)
// NewContainerInput the input for the New function
@ -45,6 +48,7 @@ type NewContainerInput struct {
NetworkMode string
Privileged bool
UsernsMode string
Platform string
}
// FileEntry is a file to copy to a container
@ -75,7 +79,7 @@ func NewContainer(input *NewContainerInput) Container {
func (cr *containerReference) Create() common.Executor {
return common.
NewDebugExecutor("%sdocker create image=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Entrypoint, cr.input.Cmd).
NewDebugExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd).
Then(
common.NewPipelineExecutor(
cr.connect(),
@ -86,7 +90,7 @@ func (cr *containerReference) Create() common.Executor {
}
func (cr *containerReference) Start(attach bool) common.Executor {
return common.
NewInfoExecutor("%sdocker run image=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Entrypoint, cr.input.Cmd).
NewInfoExecutor("%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd).
Then(
common.NewPipelineExecutor(
cr.connect(),
@ -101,6 +105,7 @@ func (cr *containerReference) Pull(forcePull bool) common.Executor {
return NewDockerPullExecutor(NewDockerPullExecutorInput{
Image: cr.input.Image,
ForcePull: forcePull,
Platform: cr.input.Platform,
})
}
func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor {
@ -267,17 +272,26 @@ func (cr *containerReference) create() common.Executor {
})
}
desiredPlatform := strings.SplitN(cr.input.Platform, `/`, 2)
if len(desiredPlatform) != 2 {
logger.Panicf("Incorrect container platform option. %s is not a valid platform.", cr.input.Platform)
}
resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
Binds: input.Binds,
Mounts: mounts,
NetworkMode: container.NetworkMode(input.NetworkMode),
Privileged: input.Privileged,
UsernsMode: container.UsernsMode(input.UsernsMode),
}, nil, input.Name)
}, nil, &specs.Platform{
Architecture: desiredPlatform[1],
OS: desiredPlatform[0],
}, 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("Created container name=%s id=%v from image %v (platform: %s)", input.Name, resp.ID, input.Image, input.Platform)
logger.Debugf("ENV ==> %v", input.Env)
cr.id = resp.ID

View file

@ -11,11 +11,11 @@ import (
"runtime"
"strings"
"github.com/nektos/act/pkg/container"
log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus"
)
// RunContext contains info about current job
@ -89,6 +89,10 @@ func (rc *RunContext) startJobContainer() common.Executor {
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, "/github/workspace", bindModifiers))
}
if rc.Config.ContainerArchitecture == "" {
rc.Config.ContainerArchitecture = fmt.Sprintf("%s/%s", "linux", runtime.GOARCH)
}
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
Cmd: nil,
Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"},
@ -107,6 +111,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
Stderr: logWriter,
Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture,
})
var copyWorkspace bool

View file

@ -17,21 +17,22 @@ type Runner interface {
// Config contains the config for a new runner
type Config struct {
Actor string // the user that triggered the event
Workdir string // path to working directory
BindWorkdir bool // bind the workdir to the job container
EventName string // name of event to run
EventPath string // path to JSON file to use for event.json in containers
DefaultBranch string // name of the main branch for this repository
ReuseContainers bool // reuse containers to maintain state
ForcePull bool // force pulling of the image, if already present
LogOutput bool // log the output from docker run
Env map[string]string // env for containers
Secrets map[string]string // list of secrets
InsecureSecrets bool // switch hiding output when printing to terminal
Platforms map[string]string // list of platforms
Privileged bool // use privileged mode
UsernsMode string // user namespace to use
Actor string // the user that triggered the event
Workdir string // path to working directory
BindWorkdir bool // bind the workdir to the job container
EventName string // name of event to run
EventPath string // path to JSON file to use for event.json in containers
DefaultBranch string // name of the main branch for this repository
ReuseContainers bool // reuse containers to maintain state
ForcePull bool // force pulling of the image, if already present
LogOutput bool // log the output from docker run
Env map[string]string // env for containers
Secrets map[string]string // list of secrets
InsecureSecrets bool // switch hiding output when printing to terminal
Platforms map[string]string // list of platforms
Privileged bool // use privileged mode
UsernsMode string // user namespace to use
ContainerArchitecture string // Desired OS/architecture platform for running containers
}
type runnerImpl struct {

View file

@ -7,10 +7,10 @@ import (
"testing"
"github.com/joho/godotenv"
"github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus"
"gotest.tools/v3/assert"
"github.com/nektos/act/pkg/model"
)
func TestGraphEvent(t *testing.T) {
@ -32,11 +32,12 @@ func TestGraphEvent(t *testing.T) {
}
type TestJobFileInfo struct {
workdir string
workflowPath string
eventName string
errorMessage string
platforms map[string]string
workdir string
workflowPath string
eventName string
errorMessage string
platforms map[string]string
containerArchitecture string
}
func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
@ -45,11 +46,12 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
assert.NilError(t, err, workdir)
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
runnerConfig := &Config{
Workdir: workdir,
BindWorkdir: true,
EventName: tjfi.eventName,
Platforms: tjfi.platforms,
ReuseContainers: false,
Workdir: workdir,
BindWorkdir: true,
EventName: tjfi.eventName,
Platforms: tjfi.platforms,
ReuseContainers: false,
ContainerArchitecture: tjfi.containerArchitecture,
}
runner, err := New(runnerConfig)
assert.NilError(t, err, tjfi.workflowPath)
@ -77,23 +79,42 @@ func TestRunEvent(t *testing.T) {
"ubuntu-latest": "node:12.20.1-buster-slim",
}
tables := []TestJobFileInfo{
{"testdata", "basic", "push", "", platforms},
{"testdata", "fail", "push", "exit with `FAILURE`: 1", platforms},
{"testdata", "runs-on", "push", "", platforms},
{"testdata", "job-container", "push", "", platforms},
{"testdata", "job-container-non-root", "push", "", platforms},
{"testdata", "uses-docker-url", "push", "", platforms},
{"testdata", "remote-action-docker", "push", "", platforms},
{"testdata", "remote-action-js", "push", "", platforms},
{"testdata", "local-action-docker-url", "push", "", platforms},
{"testdata", "local-action-dockerfile", "push", "", platforms},
{"testdata", "local-action-js", "push", "", platforms},
{"testdata", "matrix", "push", "", platforms},
{"testdata", "matrix-include-exclude", "push", "", platforms},
{"testdata", "commands", "push", "", platforms},
{"testdata", "workdir", "push", "", platforms},
// {"testdata", "issue-228", "push", "", platforms}, // TODO [igni]: Remove this once everything passes
{"testdata", "defaults-run", "push", "", platforms},
{"testdata", "basic", "push", "", platforms, "linux/amd64"},
{"testdata", "fail", "push", "exit with `FAILURE`: 1", platforms, "linux/amd64"},
{"testdata", "runs-on", "push", "", platforms, "linux/amd64"},
{"testdata", "job-container", "push", "", platforms, "linux/amd64"},
{"testdata", "job-container-non-root", "push", "", platforms, "linux/amd64"},
{"testdata", "uses-docker-url", "push", "", platforms, "linux/amd64"},
{"testdata", "remote-action-docker", "push", "", platforms, "linux/amd64"},
{"testdata", "remote-action-js", "push", "", platforms, "linux/amd64"},
{"testdata", "local-action-docker-url", "push", "", platforms, "linux/amd64"},
{"testdata", "local-action-dockerfile", "push", "", platforms, "linux/amd64"},
{"testdata", "local-action-js", "push", "", platforms, "linux/amd64"},
{"testdata", "matrix", "push", "", platforms, "linux/amd64"},
{"testdata", "matrix-include-exclude", "push", "", platforms, "linux/amd64"},
{"testdata", "commands", "push", "", platforms, "linux/amd64"},
{"testdata", "workdir", "push", "", platforms, "linux/amd64"},
// {"testdata", "issue-228", "push", "", platforms, "linux/amd64"}, // TODO [igni]: Remove this once everything passes
{"testdata", "defaults-run", "push", "", platforms, "linux/amd64"},
// linux/arm64
{"testdata", "basic", "push", "", platforms, "linux/arm64"},
{"testdata", "fail", "push", "exit with `FAILURE`: 1", platforms, "linux/arm64"},
{"testdata", "runs-on", "push", "", platforms, "linux/arm64"},
{"testdata", "job-container", "push", "", platforms, "linux/arm64"},
{"testdata", "job-container-non-root", "push", "", platforms, "linux/arm64"},
{"testdata", "uses-docker-url", "push", "", platforms, "linux/arm64"},
{"testdata", "remote-action-docker", "push", "", platforms, "linux/arm64"},
{"testdata", "remote-action-js", "push", "", platforms, "linux/arm64"},
{"testdata", "local-action-docker-url", "push", "", platforms, "linux/arm64"},
{"testdata", "local-action-dockerfile", "push", "", platforms, "linux/arm64"},
{"testdata", "local-action-js", "push", "", platforms, "linux/arm64"},
{"testdata", "matrix", "push", "", platforms, "linux/arm64"},
{"testdata", "matrix-include-exclude", "push", "", platforms, "linux/arm64"},
{"testdata", "commands", "push", "", platforms, "linux/arm64"},
{"testdata", "workdir", "push", "", platforms, "linux/arm64"},
// {"testdata", "issue-228", "push", "", platforms, "linux/arm64"}, // TODO [igni]: Remove this once everything passes
{"testdata", "defaults-run", "push", "", platforms, "linux/arm64"},
}
log.SetLevel(log.DebugLevel)

View file

@ -10,8 +10,8 @@ import (
"runtime"
"strings"
log "github.com/sirupsen/logrus"
"github.com/kballard/go-shellquote"
log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
@ -217,6 +217,10 @@ func (sc *StepContext) newStepContainer(ctx context.Context, image string, cmd [
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, "/github/workspace", bindModifiers))
}
if rc.Config.ContainerArchitecture == "" {
rc.Config.ContainerArchitecture = fmt.Sprintf("%s/%s", "linux", runtime.GOARCH)
}
stepContainer := container.NewContainer(&container.NewContainerInput{
Cmd: cmd,
Entrypoint: entrypoint,
@ -235,6 +239,7 @@ func (sc *StepContext) newStepContainer(ctx context.Context, image string, cmd [
Stderr: logWriter,
Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture,
})
return stepContainer
}
@ -354,10 +359,35 @@ func (sc *StepContext) runAction(actionDir string, actionPath string) common.Exe
image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
image = strings.ToLower(image)
contextDir := filepath.Join(actionDir, actionPath, action.Runs.Main)
exists, err := container.ImageExistsLocally(ctx, image, "any")
if err != nil {
return err
}
if exists {
wasRemoved, err := container.DeleteImage(ctx, image)
if err != nil {
return err
}
if !wasRemoved {
return fmt.Errorf("failed to delete image '%s'", image)
}
}
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
ContextDir: contextDir,
ImageTag: image,
Platform: rc.Config.ContainerArchitecture,
})
exists, err = container.ImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
if err != nil {
return err
}
if !exists {
return err
}
}
cmd, err := shellquote.Split(step.With["args"])

View file

@ -12,10 +12,14 @@ func TestStepContextExecutor(t *testing.T) {
"ubuntu-latest": "node:12.20.1-buster-slim",
}
tables := []TestJobFileInfo{
{"testdata", "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms},
{"testdata", "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms},
{"testdata", "uses-github-root", "push", "", platforms},
{"testdata", "uses-github-path", "push", "", platforms},
{"testdata", "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, "linux/amd64"},
{"testdata", "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, "linux/amd64"},
{"testdata", "uses-github-root", "push", "", platforms, "linux/amd64"},
{"testdata", "uses-github-path", "push", "", platforms, "linux/amd64"},
{"testdata", "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms, "linux/arm64"},
{"testdata", "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, "linux/arm64"},
{"testdata", "uses-github-root", "push", "", platforms, "linux/arm64"},
{"testdata", "uses-github-path", "push", "", platforms, "linux/arm64"},
}
// These tests are sufficient to only check syntax.
ctx := common.WithDryrun(context.Background(), true)