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

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

View File

@ -7,25 +7,26 @@ import (
// Input contains the input for the root command // Input contains the input for the root command
type Input struct { type Input struct {
actor string actor string
workdir string workdir string
workflowsPath string workflowsPath string
autodetectEvent bool autodetectEvent bool
eventPath string eventPath string
reuseContainers bool reuseContainers bool
bindWorkdir bool bindWorkdir bool
secrets []string secrets []string
envs []string envs []string
platforms []string platforms []string
dryrun bool dryrun bool
forcePull bool forcePull bool
noOutput bool noOutput bool
envfile string envfile string
secretfile string secretfile string
insecureSecrets bool insecureSecrets bool
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 {
@ -247,21 +249,22 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
// run the plan // run the plan
config := &runner.Config{ config := &runner.Config{
Actor: input.actor, Actor: input.actor,
EventName: eventName, EventName: eventName,
EventPath: input.EventPath(), EventPath: input.EventPath(),
DefaultBranch: defaultbranch, DefaultBranch: defaultbranch,
ForcePull: input.forcePull, ForcePull: input.forcePull,
ReuseContainers: input.reuseContainers, ReuseContainers: input.reuseContainers,
Workdir: input.Workdir(), Workdir: input.Workdir(),
BindWorkdir: input.bindWorkdir, BindWorkdir: input.bindWorkdir,
LogOutput: !input.noOutput, LogOutput: !input.noOutput,
Env: envs, Env: envs,
Secrets: secrets, Secrets: secrets,
InsecureSecrets: input.insecureSecrets, InsecureSecrets: input.insecureSecrets,
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
} }
@ -38,8 +42,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
tags := []string{input.ImageTag} tags := []string{input.ImageTag}
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

@ -17,21 +17,22 @@ type Runner interface {
// Config contains the config for a new runner // Config contains the config for a new runner
type Config struct { type Config struct {
Actor string // the user that triggered the event Actor string // the user that triggered the event
Workdir string // path to working directory Workdir string // path to working directory
BindWorkdir bool // bind the workdir to the job container BindWorkdir bool // bind the workdir to the job container
EventName string // name of event to run EventName string // name of event to run
EventPath string // path to JSON file to use for event.json in containers EventPath string // path to JSON file to use for event.json in containers
DefaultBranch string // name of the main branch for this repository DefaultBranch string // name of the main branch for this repository
ReuseContainers bool // reuse containers to maintain state ReuseContainers bool // reuse containers to maintain state
ForcePull bool // force pulling of the image, if already present ForcePull bool // force pulling of the image, if already present
LogOutput bool // log the output from docker run LogOutput bool // log the output from docker run
Env map[string]string // env for containers Env map[string]string // env for containers
Secrets map[string]string // list of secrets Secrets map[string]string // list of secrets
InsecureSecrets bool // switch hiding output when printing to terminal InsecureSecrets bool // switch hiding output when printing to terminal
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) {
@ -32,11 +32,12 @@ func TestGraphEvent(t *testing.T) {
} }
type TestJobFileInfo struct { type TestJobFileInfo struct {
workdir string workdir string
workflowPath string workflowPath string
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) {
@ -45,11 +46,12 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
assert.NilError(t, err, workdir) assert.NilError(t, err, workdir)
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath) fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
runnerConfig := &Config{ runnerConfig := &Config{
Workdir: workdir, Workdir: workdir,
BindWorkdir: true, BindWorkdir: true,
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)