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 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- uses: actions/setup-go@v1 - uses: actions/setup-go@v1
with: with:
go-version: 1.14 go-version: 1.14

View File

@ -87,11 +87,13 @@ It will save that information to `~/.actrc`, please refer to [Configuration](#co
```none ```none
-a, --actor string user that triggered the event (default "nektos/act") -a, --actor string user that triggered the event (default "nektos/act")
-b, --bind bind working directory to container, rather than copy -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 --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 ".") -C, --directory string working directory (default ".")
-n, --dryrun dryrun mode -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") --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 -e, --eventpath string path to event JSON file
-g, --graph draw workflows -g, --graph draw workflows
-h, --help help for act -h, --help help for act
@ -107,7 +109,6 @@ It will save that information to `~/.actrc`, please refer to [Configuration](#co
--secret-file string file with list of secrets to read from (e.g. --secret-file .secrets) (default ".secrets") --secret-file string file with list of secrets to read from (e.g. --secret-file .secrets) (default ".secrets")
--userns string user namespace to use --userns string user namespace to use
-v, --verbose verbose output -v, --verbose verbose output
--version version for act
-w, --watch watch the contents of the local repo and run when files change -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/") -W, --workflows string path to workflow file(s) (default "./.github/workflows/")
``` ```

View File

@ -26,6 +26,7 @@ type Input struct {
defaultBranch string defaultBranch string
privileged bool privileged bool
usernsMode string usernsMode string
containerArchitecture string
} }
func (i *Input) resolve(path string) string { func (i *Input) resolve(path string) string {

View File

@ -6,6 +6,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"strings" "strings"
"github.com/nektos/act/pkg/common" "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().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().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.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()) rootCmd.SetArgs(args())
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
@ -262,6 +264,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
Platforms: input.newPlatforms(), Platforms: input.newPlatforms(),
Privileged: input.privileged, Privileged: input.privileged,
UsernsMode: input.usernsMode, UsernsMode: input.usernsMode,
ContainerArchitecture: input.containerArchitecture,
} }
r, err := runner.New(config) r, err := runner.New(config)
if err != nil { if err != nil {

22
go.mod
View File

@ -4,44 +4,36 @@ go 1.14
require ( require (
github.com/AlecAivazis/survey/v2 v2.2.7 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/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/andreaskoch/go-fswatch v1.0.0
github.com/containerd/containerd v1.4.1 // indirect github.com/containerd/containerd v1.4.1 // indirect
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a // indirect github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a // indirect
github.com/docker/cli v20.10.0-rc1+incompatible github.com/docker/cli v20.10.3+incompatible
github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v20.10.3+incompatible
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-billy/v5 v5.0.0
github.com/go-git/go-git/v5 v5.2.0 github.com/go-git/go-git/v5 v5.2.0
github.com/go-ini/ini v1.62.0 github.com/go-ini/ini v1.62.0
github.com/golang/protobuf v1.4.3 // indirect github.com/golang/protobuf v1.4.3 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // 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/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
github.com/imdario/mergo v0.3.11 // indirect github.com/imdario/mergo v0.3.11 // indirect
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/mgutz/str v1.2.0 // indirect github.com/mgutz/str v1.2.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/moby/buildkit v0.8.1
github.com/opencontainers/image-spec v1.0.1 // indirect github.com/moby/sys/mount v0.2.0 // indirect
github.com/opencontainers/runc v0.1.1 // indirect github.com/opencontainers/image-spec v1.0.1
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
github.com/sirupsen/logrus v1.7.0 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/spf13/cobra v1.1.1
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/xanzy/ssh-agent v0.3.0 // indirect 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/net v0.0.0-20201110031124-69a78807bb2b // indirect
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // 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 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221
google.golang.org/genproto v0.0.0-20201117123952-62d171c70ae1 // indirect google.golang.org/genproto v0.0.0-20201117123952-62d171c70ae1 // indirect
google.golang.org/grpc v1.33.2 // indirect google.golang.org/grpc v1.33.2 // indirect
@ -50,5 +42,3 @@ require (
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
gotest.tools/v3 v3.0.2 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" "path/filepath"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/builder/dockerignore"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils" "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" log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
) )
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function // NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
type NewDockerBuildExecutorInput struct { type NewDockerBuildExecutorInput struct {
ContextDir string ContextDir string
ImageTag string ImageTag string
Platform string
} }
// NewDockerBuildExecutor function to create a run executor for the container // NewDockerBuildExecutor function to create a run executor for the container
func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor { func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) 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) { if common.Dryrun(ctx) {
return nil return nil
} }
@ -40,6 +44,7 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
options := types.ImageBuildOptions{ options := types.ImageBuildOptions{
Tags: tags, Tags: tags,
Remove: true, Remove: true,
Platform: input.Platform,
} }
buildContext, err := createBuildContext(input.ContextDir, "Dockerfile") buildContext, err := createBuildContext(input.ContextDir, "Dockerfile")
@ -49,7 +54,7 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
defer buildContext.Close() 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) resp, err := cli.ImageBuild(ctx, buildContext, options)
err = logDockerResponse(logger, resp.Body, err != nil) err = logDockerResponse(logger, resp.Body, err != nil)

View File

@ -2,14 +2,15 @@ package container
import ( import (
"context" "context"
"fmt"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
) )
// ImageExistsLocally returns a boolean indicating if an image with the // ImageExistsLocally returns a boolean indicating if an image with the
// requested name (and tag) exist in the local docker image store // requested name, tag and architecture exists in the local docker image store
func ImageExistsLocally(ctx context.Context, imageName string) (bool, error) { func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) {
cli, err := GetDockerClient(ctx) cli, err := GetDockerClient(ctx)
if err != nil { if err != nil {
return false, err return false, err
@ -27,5 +28,61 @@ func ImageExistsLocally(ctx context.Context, imageName string) (bool, error) {
return false, err 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 // to help make this test reliable and not flaky, we need to have
// an image that will exist, and onew that won't exist // 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.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 // pull an image
cli, err := client.NewClientWithOpts(client.FromEnv) cli, err := client.NewClientWithOpts(client.FromEnv)
@ -34,13 +40,28 @@ func TestImageExistsLocally(t *testing.T) {
// Chose alpine latest because it's so small // Chose alpine latest because it's so small
// maybe we should build an image instead so that tests aren't reliable on dockerhub // 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) assert.Nil(t, err)
defer reader.Close() defer readerDefault.Close()
_, err = ioutil.ReadAll(reader) _, err = ioutil.ReadAll(readerDefault)
assert.Nil(t, err) assert.Nil(t, err)
exists, err = ImageExistsLocally(ctx, "alpine:latest") imageDefaultArchExists, err := ImageExistsLocally(ctx, "alpine:latest", "linux/amd64")
assert.Nil(t, err) 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" "strings"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/nektos/act/pkg/common"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common"
) )
// NewDockerPullExecutorInput the input for the NewDockerPullExecutor function // NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
type NewDockerPullExecutorInput struct { type NewDockerPullExecutorInput struct {
Image string Image string
ForcePull bool ForcePull bool
Platform string
} }
// NewDockerPullExecutor function to create a run executor for the container // NewDockerPullExecutor function to create a run executor for the container
@ -29,10 +31,10 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
pull := input.ForcePull pull := input.ForcePull
if !pull { if !pull {
imageExists, err := ImageExistsLocally(ctx, input.Image) imageExists, err := ImageExistsLocally(ctx, input.Image, input.Platform)
log.Debugf("Image exists? %v", imageExists) log.Debugf("Image exists? %v", imageExists)
if err != nil { 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 { if !imageExists {
@ -45,14 +47,16 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
} }
imageRef := cleanImage(input.Image) imageRef := cleanImage(input.Image)
logger.Debugf("pulling image '%v'", imageRef) logger.Debugf("pulling image '%v' (%s)", imageRef, input.Platform)
cli, err := GetDockerClient(ctx) cli, err := GetDockerClient(ctx)
if err != nil { if err != nil {
return err 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) _ = logDockerResponse(logger, reader, err != nil)
if err != nil { if err != nil {
return err return err

View File

@ -24,10 +24,13 @@ import (
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy" "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" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/term" "golang.org/x/term"
"github.com/nektos/act/pkg/common"
) )
// NewContainerInput the input for the New function // NewContainerInput the input for the New function
@ -45,6 +48,7 @@ type NewContainerInput struct {
NetworkMode string NetworkMode string
Privileged bool Privileged bool
UsernsMode string UsernsMode string
Platform string
} }
// FileEntry is a file to copy to a container // FileEntry is a file to copy to a container
@ -75,7 +79,7 @@ func NewContainer(input *NewContainerInput) Container {
func (cr *containerReference) Create() common.Executor { func (cr *containerReference) Create() common.Executor {
return common. 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( Then(
common.NewPipelineExecutor( common.NewPipelineExecutor(
cr.connect(), cr.connect(),
@ -86,7 +90,7 @@ func (cr *containerReference) Create() common.Executor {
} }
func (cr *containerReference) Start(attach bool) common.Executor { func (cr *containerReference) Start(attach bool) common.Executor {
return common. 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( Then(
common.NewPipelineExecutor( common.NewPipelineExecutor(
cr.connect(), cr.connect(),
@ -101,6 +105,7 @@ func (cr *containerReference) Pull(forcePull bool) common.Executor {
return NewDockerPullExecutor(NewDockerPullExecutorInput{ return NewDockerPullExecutor(NewDockerPullExecutorInput{
Image: cr.input.Image, Image: cr.input.Image,
ForcePull: forcePull, ForcePull: forcePull,
Platform: cr.input.Platform,
}) })
} }
func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor { 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{ resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
Binds: input.Binds, Binds: input.Binds,
Mounts: mounts, Mounts: mounts,
NetworkMode: container.NetworkMode(input.NetworkMode), NetworkMode: container.NetworkMode(input.NetworkMode),
Privileged: input.Privileged, Privileged: input.Privileged,
UsernsMode: container.UsernsMode(input.UsernsMode), UsernsMode: container.UsernsMode(input.UsernsMode),
}, nil, input.Name) }, nil, &specs.Platform{
Architecture: desiredPlatform[1],
OS: desiredPlatform[0],
}, input.Name)
if err != nil { if err != nil {
return errors.WithStack(err) 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) logger.Debugf("ENV ==> %v", input.Env)
cr.id = resp.ID cr.id = resp.ID

View File

@ -11,11 +11,11 @@ import (
"runtime" "runtime"
"strings" "strings"
"github.com/nektos/act/pkg/container" log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus"
) )
// RunContext contains info about current job // 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)) 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{ rc.JobContainer = container.NewContainer(&container.NewContainerInput{
Cmd: nil, Cmd: nil,
Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"}, Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"},
@ -107,6 +111,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
Stderr: logWriter, Stderr: logWriter,
Privileged: rc.Config.Privileged, Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode, UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture,
}) })
var copyWorkspace bool var copyWorkspace bool

View File

@ -32,6 +32,7 @@ type Config struct {
Platforms map[string]string // list of platforms Platforms map[string]string // list of platforms
Privileged bool // use privileged mode Privileged bool // use privileged mode
UsernsMode string // user namespace to use UsernsMode string // user namespace to use
ContainerArchitecture string // Desired OS/architecture platform for running containers
} }
type runnerImpl struct { type runnerImpl struct {

View File

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

View File

@ -10,8 +10,8 @@ import (
"runtime" "runtime"
"strings" "strings"
log "github.com/sirupsen/logrus"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
log "github.com/sirupsen/logrus"
"github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container" "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)) 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{ stepContainer := container.NewContainer(&container.NewContainerInput{
Cmd: cmd, Cmd: cmd,
Entrypoint: entrypoint, Entrypoint: entrypoint,
@ -235,6 +239,7 @@ func (sc *StepContext) newStepContainer(ctx context.Context, image string, cmd [
Stderr: logWriter, Stderr: logWriter,
Privileged: rc.Config.Privileged, Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode, UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture,
}) })
return stepContainer 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 = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
image = strings.ToLower(image) image = strings.ToLower(image)
contextDir := filepath.Join(actionDir, actionPath, action.Runs.Main) 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{ prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
ContextDir: contextDir, ContextDir: contextDir,
ImageTag: image, 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"]) 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", "ubuntu-latest": "node:12.20.1-buster-slim",
} }
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
{"testdata", "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", 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}, {"testdata", "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms, "linux/amd64"},
{"testdata", "uses-github-root", "push", "", platforms}, {"testdata", "uses-github-root", "push", "", platforms, "linux/amd64"},
{"testdata", "uses-github-path", "push", "", platforms}, {"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. // These tests are sufficient to only check syntax.
ctx := common.WithDryrun(context.Background(), true) ctx := common.WithDryrun(context.Background(), true)