1
0
Fork 0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2025-08-06 17:40:58 +00:00

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 195e84f0fd
commit 391acea9e2
13 changed files with 279 additions and 111 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

@ -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)

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 {