Skip to content

image: support import command #4456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 19, 2025
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 cmd/nerdctl/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func Command() *cobra.Command {
PushCommand(),
LoadCommand(),
SaveCommand(),
ImportCommand(),
TagCommand(),
imageRemoveCommand(),
convertCommand(),
Expand Down
133 changes: 133 additions & 0 deletions cmd/nerdctl/image/image_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package image

import (
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/spf13/cobra"

"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
"github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/containerd/nerdctl/v2/pkg/clientutil"
"github.com/containerd/nerdctl/v2/pkg/cmd/image"
)

func ImportCommand() *cobra.Command {
var cmd = &cobra.Command{
Use: "import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]",
Short: "Import the contents from a tarball to create a filesystem image",
Args: cobra.MinimumNArgs(1),
RunE: importAction,
ValidArgsFunction: imageImportShellComplete,
SilenceUsage: true,
SilenceErrors: true,
}

cmd.Flags().StringP("message", "m", "", "Set commit message for imported image")
cmd.Flags().String("platform", "", "Set platform for imported image (e.g., linux/amd64)")
return cmd
}

func importOptions(cmd *cobra.Command, args []string) (types.ImageImportOptions, error) {
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImageImportOptions{}, err
}
message, err := cmd.Flags().GetString("message")
if err != nil {
return types.ImageImportOptions{}, err
}
platform, err := cmd.Flags().GetString("platform")
if err != nil {
return types.ImageImportOptions{}, err
}
var reference string
if len(args) > 1 {
reference = args[1]
}

var in io.ReadCloser
src := args[0]
switch {
case src == "-":
in = io.NopCloser(cmd.InOrStdin())
case hasHTTPPrefix(src):
resp, err := http.Get(src)
if err != nil {
return types.ImageImportOptions{}, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
defer resp.Body.Close()
return types.ImageImportOptions{}, fmt.Errorf("failed to download %s: %s", src, resp.Status)
}
in = resp.Body
default:
f, err := os.Open(src)
if err != nil {
return types.ImageImportOptions{}, err
}
in = f
}

return types.ImageImportOptions{
Stdout: cmd.OutOrStdout(),
Stdin: in,
GOptions: globalOptions,
Source: args[0],
Reference: reference,
Message: message,
Platform: platform,
}, nil
}

func importAction(cmd *cobra.Command, args []string) error {
opt, err := importOptions(cmd, args)
if err != nil {
return err
}
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), opt.GOptions.Namespace, opt.GOptions.Address)
if err != nil {
return err
}
defer cancel()
defer func() {
if rc, ok := opt.Stdin.(io.ReadCloser); ok {
_ = rc.Close()
}
}()

name, err := image.Import(ctx, client, opt)
if err != nil {
return err
}
_, err = cmd.OutOrStdout().Write([]byte(name + "\n"))
return err
}

func imageImportShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completion.ImageNames(cmd)
}

func hasHTTPPrefix(s string) bool {
return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
}
205 changes: 205 additions & 0 deletions cmd/nerdctl/image/image_import_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package image

import (
"archive/tar"
"bytes"
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"testing"

"gotest.tools/v3/assert"

"github.com/containerd/nerdctl/mod/tigron/expect"
"github.com/containerd/nerdctl/mod/tigron/require"
"github.com/containerd/nerdctl/mod/tigron/test"
"github.com/containerd/nerdctl/mod/tigron/tig"

"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)

// minimalRootfsTar returns a valid tar archive with no files.
func minimalRootfsTar(t *testing.T) *bytes.Buffer {
t.Helper()
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
assert.NilError(t, tw.Close())
return buf
}

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

testCase := &test.Case{
Description: "TestImageImportErrors",
Require: require.Linux,
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("import", "", "image:tag")
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 1,
Errors: []error{errors.New(data.Labels().Get("error"))},
}
},
Data: test.WithLabels(map[string]string{
"error": "no such file or directory",
}),
}

testCase.Run(t)
}

func TestImageImport(t *testing.T) {
testCase := nerdtest.Setup()

var stopServer func()

testCase.SubTests = []*test.Case{
{
Description: "image import from stdin",
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rmi", "-f", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
cmd := helpers.Command("import", "-", data.Identifier())
cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes()))
return cmd
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
identifier := data.Identifier()
return &test.Expected{
Output: expect.All(
func(stdout string, t tig.T) {
imgs := helpers.Capture("images")
assert.Assert(t, strings.Contains(imgs, identifier))
},
),
}
},
},
{
Description: "image import from file",
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rmi", "-f", data.Identifier())
},
Setup: func(data test.Data, helpers test.Helpers) {
p := filepath.Join(data.Temp().Path(), "rootfs.tar")
assert.NilError(t, os.WriteFile(p, minimalRootfsTar(t).Bytes(), 0644))
data.Labels().Set("tar", p)
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("import", data.Labels().Get("tar"), data.Identifier())
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
identifier := data.Identifier()
return &test.Expected{
Output: expect.All(
func(stdout string, t tig.T) {
imgs := helpers.Capture("images")
assert.Assert(t, strings.Contains(imgs, identifier))
},
),
}
},
},
{
Description: "image import with message",
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rmi", "-f", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
cmd := helpers.Command("import", "-m", "A message", "-", data.Identifier())
cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes()))
return cmd
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
identifier := data.Identifier() + ":latest"
return &test.Expected{
Output: expect.All(
func(stdout string, t tig.T) {
img := nerdtest.InspectImage(helpers, identifier)
assert.Equal(t, img.Comment, "A message")
},
),
}
},
},
{
Description: "image import with platform",
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rmi", "-f", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
cmd := helpers.Command("import", "--platform", "linux/amd64", "-", data.Identifier())
cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes()))
return cmd
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
identifier := data.Identifier() + ":latest"
return &test.Expected{
Output: expect.All(
func(stdout string, t tig.T) {
img := nerdtest.InspectImage(helpers, identifier)
assert.Equal(t, img.Architecture, "amd64")
assert.Equal(t, img.Os, "linux")
},
),
}
},
},
{
Description: "image import from URL",
Cleanup: func(data test.Data, helpers test.Helpers) {
if stopServer != nil {
stopServer()
stopServer = nil
}
helpers.Anyhow("rmi", "-f", data.Identifier())
},
Setup: func(data test.Data, helpers test.Helpers) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-tar")
_, _ = w.Write(minimalRootfsTar(t).Bytes())
})
url, stop, err := nerdtest.StartHTTPServer(handler)
assert.NilError(t, err)
stopServer = stop
data.Labels().Set("url", url)
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("import", data.Labels().Get("url"), data.Identifier())
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
identifier := data.Identifier()
return &test.Expected{
Output: expect.All(
func(stdout string, t tig.T) {
imgs := helpers.Capture("images")
assert.Assert(t, strings.Contains(imgs, identifier))
},
),
}
},
},
}
testCase.Run(t)
}
1 change: 1 addition & 0 deletions cmd/nerdctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ Config file ($NERDCTL_TOML): %s
image.PushCommand(),
image.LoadCommand(),
image.SaveCommand(),
image.ImportCommand(),
image.TagCommand(),
image.RmiCommand(),
image.HistoryCommand(),
Expand Down
15 changes: 14 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ It does not necessarily mean that the corresponding features are missing in cont
- [:whale: nerdctl push](#whale-nerdctl-push)
- [:whale: nerdctl load](#whale-nerdctl-load)
- [:whale: nerdctl save](#whale-nerdctl-save)
- [:whale: nerdctl import](#whale-nerdctl-import)
- [:whale: nerdctl tag](#whale-nerdctl-tag)
- [:whale: nerdctl rmi](#whale-nerdctl-rmi)
- [:whale: nerdctl image inspect](#whale-nerdctl-image-inspect)
Expand Down Expand Up @@ -905,6 +906,19 @@ Flags:
- :nerd_face: `--platform=(amd64|arm64|...)`: Export content for a specific platform
- :nerd_face: `--all-platforms`: Export content for all platforms

### :whale: nerdctl import

Import the contents from a tarball to create a filesystem image.

Usage: `nerdctl import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]`

Flags:

- :whale: `-m, --message`: Set commit message for imported image
- :nerd_face: `--platform=(linux/amd64|linux/arm64|...)`: Set platform for the imported image

Unimplemented `docker import` flags: `--change`

### :whale: nerdctl tag

Create a tag TARGET\_IMAGE that refers to SOURCE\_IMAGE.
Expand Down Expand Up @@ -1919,7 +1933,6 @@ Container management:

Image:

- `docker import`
- `docker trust *` (Instead, nerdctl supports `nerdctl pull --verify=cosign|notation` and `nerdctl push --sign=cosign|notation`. See [`./cosign.md`](./cosign.md) and [`./notation.md`](./notation.md).)

Network management:
Expand Down
Loading
Loading