From e4268969016e80332fadbcbf64fc08732079a249 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 9 Mar 2021 15:29:53 +0900 Subject: [PATCH 1/2] support --entrypoint Fix #87 Signed-off-by: Akihiro Suda --- README.md | 1 + build_test.go | 11 +------- pkg/testutil/testutil.go | 17 +++++++++++++ run.go | 49 ++++++++++++++++++++++++----------- run_test.go | 55 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3b9940d6059..3c4e8986ae3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build_test.go b/build_test.go index 6bcbc03ea24..53e6cad4966 100644 --- a/build_test.go +++ b/build_test.go @@ -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() diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 68b17ba9ff5..847f6abffa5 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -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" @@ -119,20 +121,24 @@ 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, } @@ -140,6 +146,7 @@ func (c *Cmd) AssertOut(s string) { } 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()) @@ -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 { diff --git a/run.go b/run.go index 25421d4c57e..6a3624c3bd2 100644 --- a/run.go +++ b/run.go @@ -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"}, @@ -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") @@ -268,31 +274,44 @@ 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) - } else { - opts = append(opts, oci.WithImageConfig(ensured.Image)) + 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 err + } + opts = append(opts, oci.WithRootFSPath(absRootfs), oci.WithDefaultPathEnv) } - if clicontext.Bool("read-only") { - opts = append(opts, oci.WithRootFSReadonly()) + // 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 errors.New("no command or entrypoint provided, and no CMD or ENTRYPOINT from image") + } + opts = append(opts, oci.WithProcessArgs(processArgs...)) } - 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") + if clicontext.Bool("read-only") { + opts = append(opts, oci.WithRootFSReadonly()) } if wd := clicontext.String("workdir"); wd != "" { diff --git a/run_test.go b/run_test.go index f22ac998527..b16f5ac3d87 100644 --- a/run_test.go +++ b/run_test.go @@ -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") @@ -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 { From a481b0a7123175b002689f2562c9089d29ea4002 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 9 Mar 2021 16:30:51 +0900 Subject: [PATCH 2/2] run: split function Signed-off-by: Akihiro Suda --- run.go | 109 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/run.go b/run.go index 6a3624c3bd2..11984a9a972 100644 --- a/run.go +++ b/run.go @@ -238,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 @@ -274,44 +265,11 @@ func runAction(clicontext *cli.Context) error { }), ) - 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 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())) + if rootfsOpts, rootfsCOpts, err := generateRootfsOpts(ctx, client, clicontext, id); err != nil { + return err } 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 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()) + opts = append(opts, rootfsOpts...) + cOpts = append(cOpts, rootfsCOpts...) } if wd := clicontext.String("workdir"); wd != "" { @@ -556,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 {