Skip to content
Open
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
33 changes: 33 additions & 0 deletions cmd/nerdctl/builder/builder_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,39 @@ CMD ["echo", "nerdctl-build-test-string"]
testCase.Run(t)
}

func TestBuildQuiet(t *testing.T) {
nerdtest.Setup()

dockerfile := fmt.Sprintf(`FROM %s
CMD ["echo", "nerdctl-build-test-string"]
`, testutil.CommonImage)

testCase := &test.Case{
Require: nerdtest.Build,
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rmi", "-f", data.Identifier())
},
Setup: func(data test.Data, helpers test.Helpers) {
data.Temp().Save(dockerfile, "Dockerfile")
// Regardless of whether the buildkit worker loads the image into the image store
// or not, `build -q` must print the image identifier on stdout.
// https://github.com/containerd/nerdctl/issues/2015
imageID := strings.TrimSpace(helpers.Capture("build", "-q", "-t", data.Identifier(), data.Temp().Path()))
assert.Assert(helpers.T(), regexp.MustCompile(`^sha256:[0-9a-f]{64}$`).MatchString(imageID),
"expected `build -q` to output a valid image ID, got %q", imageID)
data.Labels().Set("imageID", imageID)
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
// The image ID printed by `build -q` must be usable to run the built image.
return helpers.Command("run", "--rm", data.Labels().Get("imageID"))
},

Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")),
}

testCase.Run(t)
}

func TestBuildWithLabels(t *testing.T) {
nerdtest.Setup()

Expand Down
32 changes: 26 additions & 6 deletions pkg/cmd/builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,29 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder
return err
}

if options.IidFile != "" {
if metaFile != "" {
id, err := getDigestFromMetaFile(metaFile)
if err != nil {
return err
}
if err := filesystem.WriteFile(options.IidFile, []byte(id), 0644); err != nil {
return err
// A missing digest is fatal when the user explicitly asked for an iidfile, but not
// in quiet mode: the requested output may legitimately have no image digest
// (e.g. `--output type=local`).
if options.IidFile != "" {
return err
}
log.L.WithError(err).Debug("failed to get the image digest from the build metadata file")
} else {
if options.IidFile != "" {
if err := filesystem.WriteFile(options.IidFile, []byte(id), 0644); err != nil {
return err
}
}
// In quiet mode, the digest of a loaded image is printed by loadImage.
// When the image does not need loading (e.g. buildkitd with the containerd worker),
// print the digest here instead, so that `nerdctl build -q` outputs the image ID.
// https://github.com/containerd/nerdctl/issues/2015
if options.Quiet && !needsLoading {
fmt.Fprintln(options.Stdout, id)
}
}
}

Expand Down Expand Up @@ -452,7 +468,11 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
log.L.Warn("ignoring deprecated flag: '--rm=false'")
}

if options.IidFile != "" {
// The metadata file is needed to obtain the image digest: when --iidfile is passed,
// and in quiet mode when the image is not loaded (e.g. buildkitd with the containerd worker),
// in which case the digest is not printed by the load path.
// https://github.com/containerd/nerdctl/issues/2015
if options.IidFile != "" || (options.Quiet && !needsLoading) {
file, err := os.CreateTemp("", "buildkit-meta-*")
if err != nil {
return "", nil, false, "", nil, cleanup, err
Expand Down
51 changes: 51 additions & 0 deletions pkg/cmd/builder/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package builder

import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
Expand Down Expand Up @@ -281,3 +282,53 @@ func TestGetEffectiveSourcePolicyFile(t *testing.T) {
})
}
}

func TestGetDigestFromMetaFile(t *testing.T) {
t.Parallel()

const digest = "sha256:e2c8f34a2e73f9e11c93de402b9797adf95bab1e5ffb845b2cbe18f0e19dd0f1"

tests := []struct {
name string
content string
expected string
wantErr bool
}{
{
name: "digest present",
content: fmt.Sprintf(`{"containerimage.digest": %q}`, digest),
expected: digest,
},
{
name: "digest missing",
content: `{"containerimage.config.digest": "whatever"}`,
wantErr: true,
},
{
name: "invalid json",
content: `{`,
wantErr: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

path := filepath.Join(t.TempDir(), "meta.json")
assert.NilError(t, os.WriteFile(path, []byte(tc.content), 0o600))

id, err := getDigestFromMetaFile(path)
if tc.wantErr {
assert.Assert(t, err != nil)
} else {
assert.NilError(t, err)
assert.Equal(t, id, tc.expected)
}

// The metadata file is a temporary file, and must be removed once read.
_, err = os.Stat(path)
assert.Assert(t, os.IsNotExist(err))
})
}
}