Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ Rootfs flags:
Corresponds to Podman CLI.

Env flags:
- :whale: `--entrypoint`: Overwrite the default ENTRYPOINT of the image
- :whale: `-w, --workdir`: Working directory inside the container
- :whale: `-e, --env`: Set environment variables

Expand Down
11 changes: 1 addition & 10 deletions build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,13 @@ import (
"path/filepath"
"testing"

"github.com/AkihiroSuda/nerdctl/pkg/buildkitutil"
"github.com/AkihiroSuda/nerdctl/pkg/defaults"
"github.com/AkihiroSuda/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
)

func TestBuild(t *testing.T) {
testutil.RequiresBuild(t)
base := testutil.NewBase(t)
if base.Target == testutil.Nerdctl {
buildkitHost := defaults.BuildKitHost()
t.Logf("buildkitHost=%q", buildkitHost)
if err := buildkitutil.PingBKDaemon(buildkitHost); err != nil {
t.Skipf("test requires buildkitd: %+v", err)
}
}

const imageName = "nerdctl-build-test"
defer base.Cmd("rmi", imageName).Run()

Expand Down
17 changes: 17 additions & 0 deletions pkg/testutil/testutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"testing"
"time"

"github.com/AkihiroSuda/nerdctl/pkg/buildkitutil"
"github.com/AkihiroSuda/nerdctl/pkg/defaults"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
Expand Down Expand Up @@ -119,27 +121,32 @@ func (c *Cmd) Run() *icmd.Result {
}

func (c *Cmd) Assert(expected icmd.Expected) {
c.Base.T.Helper()
c.Run().Assert(c.Base.T, expected)
}

func (c *Cmd) AssertOK() {
c.Base.T.Helper()
expected := icmd.Expected{}
c.Assert(expected)
}

func (c *Cmd) AssertFail() {
c.Base.T.Helper()
res := c.Run()
assert.Assert(c.Base.T, res.ExitCode != 0)
}

func (c *Cmd) AssertOut(s string) {
c.Base.T.Helper()
expected := icmd.Expected{
Out: s,
}
c.Assert(expected)
}

func (c *Cmd) AssertOutWithFunc(fn func(stdout string) error) {
c.Base.T.Helper()
res := c.Run()
assert.Equal(c.Base.T, 0, res.ExitCode, res.Combined())
assert.NilError(c.Base.T, fn(res.Stdout()), res.Combined())
Expand Down Expand Up @@ -182,6 +189,16 @@ func DockerIncompatible(t testing.TB) {
}
}

func RequiresBuild(t testing.TB) {
if GetTarget() == Nerdctl {
buildkitHost := defaults.BuildKitHost()
t.Logf("buildkitHost=%q", buildkitHost)
if err := buildkitutil.PingBKDaemon(buildkitHost); err != nil {
t.Skipf("test requires buildkitd: %+v", err)
}
}
}

const Namespace = "nerdctl-test"

func NewBase(t *testing.T) *Base {
Expand Down
102 changes: 69 additions & 33 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ var runCommand = &cli.Command{
Usage: "The first argument is not an image but the rootfs to the exploded container",
},
// env flags
&cli.StringFlag{
Name: "entrypoint",
Usage: "Overwrite the default ENTRYPOINT of the image",
},
&cli.StringFlag{
Name: "workdir",
Aliases: []string{"w"},
Expand Down Expand Up @@ -215,6 +219,8 @@ var runCommand = &cli.Command{

// runAction is heavily based on ctr implementation:
// https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/run/run.go
//
// FIXME: split to smaller functions
func runAction(clicontext *cli.Context) error {
if clicontext.Bool("help") {
return cli.ShowCommandHelp(clicontext, "run")
Expand All @@ -232,15 +238,6 @@ func runAction(clicontext *cli.Context) error {
}
defer cancel()

imageless := clicontext.Bool("rootfs")
var ensured *imgutil.EnsuredImage
if !imageless {
ensured, err = imgutil.EnsureImage(ctx, client, clicontext.App.Writer, clicontext.String("snapshotter"), clicontext.Args().First(),
clicontext.String("pull"), clicontext.Bool("insecure-registry"))
if err != nil {
return err
}
}
var (
opts []oci.SpecOpts
cOpts []containerd.NewContainerOpts
Expand Down Expand Up @@ -268,31 +265,11 @@ func runAction(clicontext *cli.Context) error {
}),
)

if imageless {
absRootfs, err := filepath.Abs(clicontext.Args().First())
if err != nil {
return err
}
opts = append(opts, oci.WithRootFSPath(absRootfs), oci.WithDefaultPathEnv)
if rootfsOpts, rootfsCOpts, err := generateRootfsOpts(ctx, client, clicontext, id); err != nil {
return err
} else {
opts = append(opts, oci.WithImageConfig(ensured.Image))
cOpts = append(cOpts,
containerd.WithImage(ensured.Image),
containerd.WithSnapshotter(ensured.Snapshotter),
containerd.WithNewSnapshot(id, ensured.Image),
containerd.WithImageStopSignal(ensured.Image, "SIGTERM"),
)
}

if clicontext.Bool("read-only") {
opts = append(opts, oci.WithRootFSReadonly())
}

if clicontext.NArg() > 1 {
opts = append(opts, oci.WithProcessArgs(clicontext.Args().Tail()...))
} else if imageless {
// error message is from Podman
return errors.New("no command or entrypoint provided, and no CMD or ENTRYPOINT from image")
opts = append(opts, rootfsOpts...)
cOpts = append(cOpts, rootfsCOpts...)
}

if wd := clicontext.String("workdir"); wd != "" {
Expand Down Expand Up @@ -537,6 +514,65 @@ func runAction(clicontext *cli.Context) error {
return nil
}

func generateRootfsOpts(ctx context.Context, client *containerd.Client, clicontext *cli.Context, id string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) {
imageless := clicontext.Bool("rootfs")
var (
ensured *imgutil.EnsuredImage
err error
)
if !imageless {
ensured, err = imgutil.EnsureImage(ctx, client, clicontext.App.Writer, clicontext.String("snapshotter"), clicontext.Args().First(),
clicontext.String("pull"), clicontext.Bool("insecure-registry"))
if err != nil {
return nil, nil, err
}
}
var (
opts []oci.SpecOpts
cOpts []containerd.NewContainerOpts
)
if !imageless {
cOpts = append(cOpts,
containerd.WithImage(ensured.Image),
containerd.WithSnapshotter(ensured.Snapshotter),
containerd.WithNewSnapshot(id, ensured.Image),
containerd.WithImageStopSignal(ensured.Image, "SIGTERM"),
)
} else {
absRootfs, err := filepath.Abs(clicontext.Args().First())
if err != nil {
return nil, nil, err
}
opts = append(opts, oci.WithRootFSPath(absRootfs), oci.WithDefaultPathEnv)
}

// NOTE: "--entrypoint" can be set to an empty string, see TestRunEntrypoint* in run_test.go .
if !imageless && !clicontext.IsSet("entrypoint") {
opts = append(opts, oci.WithImageConfigArgs(ensured.Image, clicontext.Args().Tail()))
} else {
if !imageless {
opts = append(opts, oci.WithImageConfig(ensured.Image))
}
var processArgs []string
if entrypoint := clicontext.String("entrypoint"); entrypoint != "" {
processArgs = append(processArgs, entrypoint)
}
if clicontext.NArg() > 1 {
processArgs = append(processArgs, clicontext.Args().Tail()...)
}
if len(processArgs) == 0 {
// error message is from Podman
return nil, nil, errors.New("no command or entrypoint provided, and no CMD or ENTRYPOINT from image")
}
opts = append(opts, oci.WithProcessArgs(processArgs...))
}

if clicontext.Bool("read-only") {
opts = append(opts, oci.WithRootFSReadonly())
}
return opts, cOpts, nil
}

func genID() string {
h := sha256.New()
if err := binary.Write(h, binary.LittleEndian, time.Now().UnixNano()); err != nil {
Expand Down
55 changes: 55 additions & 0 deletions run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,69 @@
package main

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"

"github.com/AkihiroSuda/nerdctl/pkg/testutil"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)

func TestRunEntrypointWithBuild(t *testing.T) {
testutil.RequiresBuild(t)
base := testutil.NewBase(t)
const imageName = "nerdctl-test-entrypoint-with-build"
defer base.Cmd("rmi", imageName).Run()

dockerfile := fmt.Sprintf(`FROM %s
ENTRYPOINT ["echo", "foo"]
CMD ["echo", "bar"]
`, testutil.AlpineImage)

buildCtx, err := createBuildContext(dockerfile)
assert.NilError(t, err)
defer os.RemoveAll(buildCtx)

base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
base.Cmd("run", "--rm", imageName).AssertOutWithFunc(func(stdout string) error {
expected := "foo echo bar\n"
if stdout != expected {
return errors.Errorf("expected %q, got %q", expected, stdout)
}
return nil
})
base.Cmd("run", "--rm", "--entrypoint", "", imageName).AssertFail()
base.Cmd("run", "--rm", "--entrypoint", "", imageName, "echo", "blah").AssertOutWithFunc(func(stdout string) error {
if !strings.Contains(stdout, "blah") {
return errors.New("echo blah was not executed?")
}
if strings.Contains(stdout, "bar") {
return errors.New("echo bar should not be executed")
}
if strings.Contains(stdout, "foo") {
return errors.New("echo foo should not be executed")
}
return nil
})
base.Cmd("run", "--rm", "--entrypoint", "time", imageName).AssertFail()
base.Cmd("run", "--rm", "--entrypoint", "time", imageName, "echo", "blah").AssertOutWithFunc(func(stdout string) error {
if !strings.Contains(stdout, "blah") {
return errors.New("echo blah was not executed?")
}
if strings.Contains(stdout, "bar") {
return errors.New("echo bar should not be executed")
}
if strings.Contains(stdout, "foo") {
return errors.New("echo foo should not be executed")
}
return nil
})
}

func TestRunWorkdir(t *testing.T) {
base := testutil.NewBase(t)
cmd := base.Cmd("run", "--rm", "--workdir=/foo", testutil.AlpineImage, "pwd")
Expand All @@ -39,6 +93,7 @@ func TestRunCustomRootfs(t *testing.T) {
rootfs := prepareCustomRootfs(base, testutil.AlpineImage)
defer os.RemoveAll(rootfs)
base.Cmd("run", "--rm", "--rootfs", rootfs, "/bin/cat", "/proc/self/environ").AssertOut("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
base.Cmd("run", "--rm", "--entrypoint", "/bin/echo", "--rootfs", rootfs, "echo", "foo").AssertOut("echo foo")
}

func prepareCustomRootfs(base *testutil.Base, imageName string) string {
Expand Down