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:
@@ -7,24 +7,28 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/builder/dockerignore"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
|
||||
// github.com/docker/docker/builder/dockerignore is deprecated
|
||||
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
)
|
||||
|
||||
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
|
||||
type NewDockerBuildExecutorInput struct {
|
||||
ContextDir string
|
||||
ImageTag string
|
||||
Platform string
|
||||
}
|
||||
|
||||
// NewDockerBuildExecutor function to create a run executor for the container
|
||||
func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
logger.Infof("%sdocker build -t %s %s", logPrefix, input.ImageTag, input.ContextDir)
|
||||
logger.Infof("%sdocker build -t %s --platform %s %s", logPrefix, input.ImageTag, input.Platform, input.ContextDir)
|
||||
if common.Dryrun(ctx) {
|
||||
return nil
|
||||
}
|
||||
@@ -38,8 +42,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
||||
|
||||
tags := []string{input.ImageTag}
|
||||
options := types.ImageBuildOptions{
|
||||
Tags: tags,
|
||||
Remove: true,
|
||||
Tags: tags,
|
||||
Remove: true,
|
||||
Platform: input.Platform,
|
||||
}
|
||||
|
||||
buildContext, err := createBuildContext(input.ContextDir, "Dockerfile")
|
||||
@@ -49,7 +54,7 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
||||
|
||||
defer buildContext.Close()
|
||||
|
||||
logger.Debugf("Creating image from context dir '%s' with tag '%s'", input.ContextDir, input.ImageTag)
|
||||
logger.Debugf("Creating image from context dir '%s' with tag '%s' and platform '%s'", input.ContextDir, input.ImageTag, input.Platform)
|
||||
resp, err := cli.ImageBuild(ctx, buildContext, options)
|
||||
|
||||
err = logDockerResponse(logger, resp.Body, err != nil)
|
||||
|
@@ -2,14 +2,15 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
|
||||
// ImageExistsLocally returns a boolean indicating if an image with the
|
||||
// requested name (and tag) exist in the local docker image store
|
||||
func ImageExistsLocally(ctx context.Context, imageName string) (bool, error) {
|
||||
// requested name, tag and architecture exists in the local docker image store
|
||||
func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) {
|
||||
cli, err := GetDockerClient(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -27,5 +28,61 @@ func ImageExistsLocally(ctx context.Context, imageName string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return len(images) > 0, nil
|
||||
if len(images) > 0 {
|
||||
if platform == "any" {
|
||||
return true, nil
|
||||
}
|
||||
for _, v := range images {
|
||||
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, v.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if fmt.Sprintf("%s/%s", inspectImage.Os, inspectImage.Architecture) == platform {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// DeleteImage removes image from local store, the function is used to run different
|
||||
// container image architectures
|
||||
func DeleteImage(ctx context.Context, imageName string) (bool, error) {
|
||||
if exists, err := ImageExistsLocally(ctx, imageName, "any"); !exists {
|
||||
return false, err
|
||||
}
|
||||
|
||||
cli, err := GetDockerClient(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("reference", imageName)
|
||||
|
||||
imageListOptions := types.ImageListOptions{
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
images, err := cli.ImageList(ctx, imageListOptions)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(images) > 0 {
|
||||
for _, v := range images {
|
||||
if _, err = cli.ImageRemove(ctx, v.ID, types.ImageRemoveOptions{
|
||||
Force: true,
|
||||
PruneChildren: true,
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
@@ -23,9 +23,15 @@ func TestImageExistsLocally(t *testing.T) {
|
||||
// to help make this test reliable and not flaky, we need to have
|
||||
// an image that will exist, and onew that won't exist
|
||||
|
||||
exists, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist")
|
||||
// Test if image exists with specific tag
|
||||
invalidImageTag, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, false, exists)
|
||||
assert.Equal(t, false, invalidImageTag)
|
||||
|
||||
// Test if image exists with specific architecture (image platform)
|
||||
invalidImagePlatform, err := ImageExistsLocally(ctx, "alpine:latest", "windows/amd64")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, false, invalidImagePlatform)
|
||||
|
||||
// pull an image
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
@@ -34,13 +40,28 @@ func TestImageExistsLocally(t *testing.T) {
|
||||
|
||||
// Chose alpine latest because it's so small
|
||||
// maybe we should build an image instead so that tests aren't reliable on dockerhub
|
||||
reader, err := cli.ImagePull(ctx, "alpine:latest", types.ImagePullOptions{})
|
||||
readerDefault, err := cli.ImagePull(ctx, "alpine:latest", types.ImagePullOptions{
|
||||
Platform: "linux/amd64",
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
defer reader.Close()
|
||||
_, err = ioutil.ReadAll(reader)
|
||||
defer readerDefault.Close()
|
||||
_, err = ioutil.ReadAll(readerDefault)
|
||||
assert.Nil(t, err)
|
||||
|
||||
exists, err = ImageExistsLocally(ctx, "alpine:latest")
|
||||
imageDefaultArchExists, err := ImageExistsLocally(ctx, "alpine:latest", "linux/amd64")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, true, exists)
|
||||
assert.Equal(t, true, imageDefaultArchExists)
|
||||
|
||||
// Validate if another architecture platform can be pulled
|
||||
readerArm64, err := cli.ImagePull(ctx, "alpine:latest", types.ImagePullOptions{
|
||||
Platform: "linux/arm64",
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
defer readerArm64.Close()
|
||||
_, err = ioutil.ReadAll(readerArm64)
|
||||
assert.Nil(t, err)
|
||||
|
||||
imageArm64Exists, err := ImageExistsLocally(ctx, "alpine:latest", "linux/arm64")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, true, imageArm64Exists)
|
||||
}
|
||||
|
@@ -6,15 +6,17 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
)
|
||||
|
||||
// NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
|
||||
type NewDockerPullExecutorInput struct {
|
||||
Image string
|
||||
ForcePull bool
|
||||
Platform string
|
||||
}
|
||||
|
||||
// NewDockerPullExecutor function to create a run executor for the container
|
||||
@@ -29,10 +31,10 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
||||
|
||||
pull := input.ForcePull
|
||||
if !pull {
|
||||
imageExists, err := ImageExistsLocally(ctx, input.Image)
|
||||
imageExists, err := ImageExistsLocally(ctx, input.Image, input.Platform)
|
||||
log.Debugf("Image exists? %v", imageExists)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "unable to determine if image already exists for image %q", input.Image)
|
||||
return errors.WithMessagef(err, "unable to determine if image already exists for image %q (%s)", input.Image, input.Platform)
|
||||
}
|
||||
|
||||
if !imageExists {
|
||||
@@ -45,14 +47,16 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
||||
}
|
||||
|
||||
imageRef := cleanImage(input.Image)
|
||||
logger.Debugf("pulling image '%v'", imageRef)
|
||||
logger.Debugf("pulling image '%v' (%s)", imageRef, input.Platform)
|
||||
|
||||
cli, err := GetDockerClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader, err := cli.ImagePull(ctx, imageRef, types.ImagePullOptions{})
|
||||
reader, err := cli.ImagePull(ctx, imageRef, types.ImagePullOptions{
|
||||
Platform: input.Platform,
|
||||
})
|
||||
_ = logDockerResponse(logger, reader, err != nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -24,10 +24,13 @@ import (
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
)
|
||||
|
||||
// NewContainerInput the input for the New function
|
||||
@@ -45,6 +48,7 @@ type NewContainerInput struct {
|
||||
NetworkMode string
|
||||
Privileged bool
|
||||
UsernsMode string
|
||||
Platform string
|
||||
}
|
||||
|
||||
// FileEntry is a file to copy to a container
|
||||
@@ -75,7 +79,7 @@ func NewContainer(input *NewContainerInput) Container {
|
||||
|
||||
func (cr *containerReference) Create() common.Executor {
|
||||
return common.
|
||||
NewDebugExecutor("%sdocker create image=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Entrypoint, cr.input.Cmd).
|
||||
NewDebugExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd).
|
||||
Then(
|
||||
common.NewPipelineExecutor(
|
||||
cr.connect(),
|
||||
@@ -86,7 +90,7 @@ func (cr *containerReference) Create() common.Executor {
|
||||
}
|
||||
func (cr *containerReference) Start(attach bool) common.Executor {
|
||||
return common.
|
||||
NewInfoExecutor("%sdocker run image=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Entrypoint, cr.input.Cmd).
|
||||
NewInfoExecutor("%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd).
|
||||
Then(
|
||||
common.NewPipelineExecutor(
|
||||
cr.connect(),
|
||||
@@ -101,6 +105,7 @@ func (cr *containerReference) Pull(forcePull bool) common.Executor {
|
||||
return NewDockerPullExecutor(NewDockerPullExecutorInput{
|
||||
Image: cr.input.Image,
|
||||
ForcePull: forcePull,
|
||||
Platform: cr.input.Platform,
|
||||
})
|
||||
}
|
||||
func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor {
|
||||
@@ -267,17 +272,26 @@ func (cr *containerReference) create() common.Executor {
|
||||
})
|
||||
}
|
||||
|
||||
desiredPlatform := strings.SplitN(cr.input.Platform, `/`, 2)
|
||||
|
||||
if len(desiredPlatform) != 2 {
|
||||
logger.Panicf("Incorrect container platform option. %s is not a valid platform.", cr.input.Platform)
|
||||
}
|
||||
|
||||
resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
|
||||
Binds: input.Binds,
|
||||
Mounts: mounts,
|
||||
NetworkMode: container.NetworkMode(input.NetworkMode),
|
||||
Privileged: input.Privileged,
|
||||
UsernsMode: container.UsernsMode(input.UsernsMode),
|
||||
}, nil, input.Name)
|
||||
}, nil, &specs.Platform{
|
||||
Architecture: desiredPlatform[1],
|
||||
OS: desiredPlatform[0],
|
||||
}, input.Name)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image)
|
||||
logger.Debugf("Created container name=%s id=%v from image %v (platform: %s)", input.Name, resp.ID, input.Image, input.Platform)
|
||||
logger.Debugf("ENV ==> %v", input.Env)
|
||||
|
||||
cr.id = resp.ID
|
||||
|
Reference in New Issue
Block a user