From fe8ffa5d582569e1e29505efffde4031548c1458 Mon Sep 17 00:00:00 2001 From: Tushar Gupta Date: Thu, 5 Jun 2025 02:30:19 +0530 Subject: [PATCH] feat: add support for DockerfileInline As nerdctl currently uses "nerdctl build" to build the service images, write the inline file to a temporary file and use "-f" to specify the temporary dockerfile. Signed-off-by: Tushar Gupta --- .../compose/compose_build_linux_test.go | 37 ++++++++++++++++--- pkg/composer/build.go | 17 +++++++++ pkg/composer/serviceparser/build.go | 6 ++- pkg/composer/serviceparser/build_test.go | 20 ++++++++++ pkg/composer/serviceparser/serviceparser.go | 5 ++- 5 files changed, 77 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/compose/compose_build_linux_test.go b/cmd/nerdctl/compose/compose_build_linux_test.go index 79ef29a178b..cfa51e27400 100644 --- a/cmd/nerdctl/compose/compose_build_linux_test.go +++ b/cmd/nerdctl/compose/compose_build_linux_test.go @@ -39,6 +39,7 @@ func TestComposeBuild(t *testing.T) { // Make sure we shard the image name to something unique to the test to avoid conflicts with other tests imageSvc0 := data.Identifier("svc0") imageSvc1 := data.Identifier("svc1") + imageSvc2 := data.Identifier("svc2") // We are not going to run them, so, ports conflicts should not matter here dockerComposeYAML := fmt.Sprintf(` @@ -51,7 +52,13 @@ services: svc1: build: . image: %s -`, imageSvc0, imageSvc1) + svc2: + image: %s + build: + context: . + dockerfile_inline: | + FROM %s +`, imageSvc0, imageSvc1, imageSvc2, testutil.CommonImage) data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Temp().Save(dockerfile, "Dockerfile") @@ -59,6 +66,7 @@ services: data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) data.Labels().Set("imageSvc0", imageSvc0) data.Labels().Set("imageSvc1", imageSvc1) + data.Labels().Set("imageSvc2", imageSvc2) } testCase.SubTests = []*test.Case{ @@ -76,22 +84,41 @@ services: Output: expect.All( expect.Contains(data.Labels().Get("imageSvc0")), expect.DoesNotContain(data.Labels().Get("imageSvc1")), + expect.DoesNotContain(data.Labels().Get("imageSvc2")), + ), + } + }, + }, + { + Description: "build svc2", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc2") + }, + + Command: test.Command("images"), + + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.All( + expect.Contains(data.Labels().Get("imageSvc2")), + expect.DoesNotContain(data.Labels().Get("imageSvc1")), ), } }, }, { - Description: "build svc0 and svc1", + Description: "build svc0, svc1, svc2", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0", "svc1") + helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0", "svc1", "svc2") }, Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1")), + Output: expect.Contains(data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1"), data.Labels().Get("imageSvc2")), } }, }, @@ -122,7 +149,7 @@ services: testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("imageSvc0") != "" { - helpers.Anyhow("rmi", data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1")) + helpers.Anyhow("rmi", data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1"), data.Labels().Get("imageSvc2")) } } diff --git a/pkg/composer/build.go b/pkg/composer/build.go index 17b3fd0d8cd..780c7d8c319 100644 --- a/pkg/composer/build.go +++ b/pkg/composer/build.go @@ -63,6 +63,23 @@ func (c *Composer) buildServiceImage(ctx context.Context, image string, b *servi if bo.Progress != "" { args = append(args, "--progress="+bo.Progress) } + + if b.DockerfileInline != "" { + // if DockerfileInline is specified, write it to a temporary file + // and use -f flag to use that docker file with project's ctxdir + tmpFile, err := os.CreateTemp("", "inline-dockerfile-*.Dockerfile") + if err != nil { + return fmt.Errorf("failed to create temp file for DockerfileInline: %w", err) + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + if _, err := tmpFile.Write([]byte(b.DockerfileInline)); err != nil { + return fmt.Errorf("failed to write DockerfileInline: %w", err) + } + b.BuildArgs = append(b.BuildArgs, "-f="+tmpFile.Name()) + } + args = append(args, b.BuildArgs...) cmd := c.createNerdctlCmd(ctx, append([]string{"build"}, args...)...) diff --git a/pkg/composer/serviceparser/build.go b/pkg/composer/serviceparser/build.go index d797d0690ba..98839a5c396 100644 --- a/pkg/composer/serviceparser/build.go +++ b/pkg/composer/serviceparser/build.go @@ -34,7 +34,7 @@ import ( func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName string) (*Build, error) { if unknown := reflectutil.UnknownNonEmptyFields(c, - "Context", "Dockerfile", "Args", "CacheFrom", "Target", "Labels", "Secrets", "AdditionalContexts", + "Context", "Dockerfile", "Args", "CacheFrom", "Target", "Labels", "Secrets", "DockerfileInline", "AdditionalContexts", ); len(unknown) > 0 { log.L.Warnf("Ignoring: build: %+v", unknown) } @@ -60,6 +60,10 @@ func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName st } } + if c.DockerfileInline != "" { + b.DockerfileInline = c.DockerfileInline + } + for k, v := range c.Args { if v == nil { b.BuildArgs = append(b.BuildArgs, "--build-arg="+k) diff --git a/pkg/composer/serviceparser/build_test.go b/pkg/composer/serviceparser/build_test.go index 34af7143aec..e152296c242 100644 --- a/pkg/composer/serviceparser/build_test.go +++ b/pkg/composer/serviceparser/build_test.go @@ -18,6 +18,7 @@ package serviceparser import ( "runtime" + "strings" "testing" "gotest.tools/v3/assert" @@ -54,6 +55,12 @@ services: target: tgt_secret - simple_secret - absolute_secret + baz: + image: bazimg + build: + context: ./bazctx + dockerfile_inline: | + FROM random secrets: src_secret: file: test_secret1 @@ -95,4 +102,17 @@ secrets: assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=tgt_secret,src="+secretPath+"/test_secret1")) assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=simple_secret,src="+secretPath+"/test_secret2")) assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=absolute_secret,src=/tmp/absolute_secret")) + + bazSvc, err := project.GetService("baz") + assert.NilError(t, err) + + baz, err := Parse(project, bazSvc) + assert.NilError(t, err) + + t.Logf("baz: %+v", baz) + t.Logf("baz.Build.BuildArgs: %+v", baz.Build.BuildArgs) + t.Logf("baz.Build.DockerfileInline: %q", baz.Build.DockerfileInline) + assert.Assert(t, func() bool { + return strings.TrimSpace(baz.Build.DockerfileInline) == "FROM random" + }()) } diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 971e4d8041b..804250f80ec 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -195,8 +195,9 @@ type Container struct { } type Build struct { - Force bool // force build even if already present - BuildArgs []string // {"-t", "example.com/foo", "--target", "foo", "/path/to/ctx"} + Force bool // force build even if already present + BuildArgs []string // {"-t", "example.com/foo", "--target", "foo", "/path/to/ctx"} + DockerfileInline string // store contents of dockerfile_inline field is specified // TODO: call BuildKit API directly without executing `nerdctl build` }