Skip to content

Commit 2b4e092

Browse files
authored
Merge pull request #4456 from ChengyuZhu6/import
image: support import command
2 parents 2674b07 + c01827d commit 2b4e092

File tree

8 files changed

+665
-1
lines changed

8 files changed

+665
-1
lines changed

cmd/nerdctl/image/image.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func Command() *cobra.Command {
4141
PushCommand(),
4242
LoadCommand(),
4343
SaveCommand(),
44+
ImportCommand(),
4445
TagCommand(),
4546
imageRemoveCommand(),
4647
convertCommand(),

cmd/nerdctl/image/image_import.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package image
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"net/http"
23+
"os"
24+
"strings"
25+
26+
"github.com/spf13/cobra"
27+
28+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
29+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
30+
"github.com/containerd/nerdctl/v2/pkg/api/types"
31+
"github.com/containerd/nerdctl/v2/pkg/clientutil"
32+
"github.com/containerd/nerdctl/v2/pkg/cmd/image"
33+
)
34+
35+
func ImportCommand() *cobra.Command {
36+
var cmd = &cobra.Command{
37+
Use: "import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]",
38+
Short: "Import the contents from a tarball to create a filesystem image",
39+
Args: cobra.MinimumNArgs(1),
40+
RunE: importAction,
41+
ValidArgsFunction: imageImportShellComplete,
42+
SilenceUsage: true,
43+
SilenceErrors: true,
44+
}
45+
46+
cmd.Flags().StringP("message", "m", "", "Set commit message for imported image")
47+
cmd.Flags().String("platform", "", "Set platform for imported image (e.g., linux/amd64)")
48+
return cmd
49+
}
50+
51+
func importOptions(cmd *cobra.Command, args []string) (types.ImageImportOptions, error) {
52+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
53+
if err != nil {
54+
return types.ImageImportOptions{}, err
55+
}
56+
message, err := cmd.Flags().GetString("message")
57+
if err != nil {
58+
return types.ImageImportOptions{}, err
59+
}
60+
platform, err := cmd.Flags().GetString("platform")
61+
if err != nil {
62+
return types.ImageImportOptions{}, err
63+
}
64+
var reference string
65+
if len(args) > 1 {
66+
reference = args[1]
67+
}
68+
69+
var in io.ReadCloser
70+
src := args[0]
71+
switch {
72+
case src == "-":
73+
in = io.NopCloser(cmd.InOrStdin())
74+
case hasHTTPPrefix(src):
75+
resp, err := http.Get(src)
76+
if err != nil {
77+
return types.ImageImportOptions{}, err
78+
}
79+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
80+
defer resp.Body.Close()
81+
return types.ImageImportOptions{}, fmt.Errorf("failed to download %s: %s", src, resp.Status)
82+
}
83+
in = resp.Body
84+
default:
85+
f, err := os.Open(src)
86+
if err != nil {
87+
return types.ImageImportOptions{}, err
88+
}
89+
in = f
90+
}
91+
92+
return types.ImageImportOptions{
93+
Stdout: cmd.OutOrStdout(),
94+
Stdin: in,
95+
GOptions: globalOptions,
96+
Source: args[0],
97+
Reference: reference,
98+
Message: message,
99+
Platform: platform,
100+
}, nil
101+
}
102+
103+
func importAction(cmd *cobra.Command, args []string) error {
104+
opt, err := importOptions(cmd, args)
105+
if err != nil {
106+
return err
107+
}
108+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), opt.GOptions.Namespace, opt.GOptions.Address)
109+
if err != nil {
110+
return err
111+
}
112+
defer cancel()
113+
defer func() {
114+
if rc, ok := opt.Stdin.(io.ReadCloser); ok {
115+
_ = rc.Close()
116+
}
117+
}()
118+
119+
name, err := image.Import(ctx, client, opt)
120+
if err != nil {
121+
return err
122+
}
123+
_, err = cmd.OutOrStdout().Write([]byte(name + "\n"))
124+
return err
125+
}
126+
127+
func imageImportShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
128+
return completion.ImageNames(cmd)
129+
}
130+
131+
func hasHTTPPrefix(s string) bool {
132+
return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
133+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package image
18+
19+
import (
20+
"archive/tar"
21+
"bytes"
22+
"errors"
23+
"net/http"
24+
"os"
25+
"path/filepath"
26+
"strings"
27+
"testing"
28+
29+
"gotest.tools/v3/assert"
30+
31+
"github.com/containerd/nerdctl/mod/tigron/expect"
32+
"github.com/containerd/nerdctl/mod/tigron/require"
33+
"github.com/containerd/nerdctl/mod/tigron/test"
34+
"github.com/containerd/nerdctl/mod/tigron/tig"
35+
36+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
37+
)
38+
39+
// minimalRootfsTar returns a valid tar archive with no files.
40+
func minimalRootfsTar(t *testing.T) *bytes.Buffer {
41+
t.Helper()
42+
buf := new(bytes.Buffer)
43+
tw := tar.NewWriter(buf)
44+
assert.NilError(t, tw.Close())
45+
return buf
46+
}
47+
48+
func TestImageImportErrors(t *testing.T) {
49+
nerdtest.Setup()
50+
51+
testCase := &test.Case{
52+
Description: "TestImageImportErrors",
53+
Require: require.Linux,
54+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
55+
return helpers.Command("import", "", "image:tag")
56+
},
57+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
58+
return &test.Expected{
59+
ExitCode: 1,
60+
Errors: []error{errors.New(data.Labels().Get("error"))},
61+
}
62+
},
63+
Data: test.WithLabels(map[string]string{
64+
"error": "no such file or directory",
65+
}),
66+
}
67+
68+
testCase.Run(t)
69+
}
70+
71+
func TestImageImport(t *testing.T) {
72+
testCase := nerdtest.Setup()
73+
74+
var stopServer func()
75+
76+
testCase.SubTests = []*test.Case{
77+
{
78+
Description: "image import from stdin",
79+
Cleanup: func(data test.Data, helpers test.Helpers) {
80+
helpers.Anyhow("rmi", "-f", data.Identifier())
81+
},
82+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
83+
cmd := helpers.Command("import", "-", data.Identifier())
84+
cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes()))
85+
return cmd
86+
},
87+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
88+
identifier := data.Identifier()
89+
return &test.Expected{
90+
Output: expect.All(
91+
func(stdout string, t tig.T) {
92+
imgs := helpers.Capture("images")
93+
assert.Assert(t, strings.Contains(imgs, identifier))
94+
},
95+
),
96+
}
97+
},
98+
},
99+
{
100+
Description: "image import from file",
101+
Cleanup: func(data test.Data, helpers test.Helpers) {
102+
helpers.Anyhow("rmi", "-f", data.Identifier())
103+
},
104+
Setup: func(data test.Data, helpers test.Helpers) {
105+
p := filepath.Join(data.Temp().Path(), "rootfs.tar")
106+
assert.NilError(t, os.WriteFile(p, minimalRootfsTar(t).Bytes(), 0644))
107+
data.Labels().Set("tar", p)
108+
},
109+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
110+
return helpers.Command("import", data.Labels().Get("tar"), data.Identifier())
111+
},
112+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
113+
identifier := data.Identifier()
114+
return &test.Expected{
115+
Output: expect.All(
116+
func(stdout string, t tig.T) {
117+
imgs := helpers.Capture("images")
118+
assert.Assert(t, strings.Contains(imgs, identifier))
119+
},
120+
),
121+
}
122+
},
123+
},
124+
{
125+
Description: "image import with message",
126+
Cleanup: func(data test.Data, helpers test.Helpers) {
127+
helpers.Anyhow("rmi", "-f", data.Identifier())
128+
},
129+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
130+
cmd := helpers.Command("import", "-m", "A message", "-", data.Identifier())
131+
cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes()))
132+
return cmd
133+
},
134+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
135+
identifier := data.Identifier() + ":latest"
136+
return &test.Expected{
137+
Output: expect.All(
138+
func(stdout string, t tig.T) {
139+
img := nerdtest.InspectImage(helpers, identifier)
140+
assert.Equal(t, img.Comment, "A message")
141+
},
142+
),
143+
}
144+
},
145+
},
146+
{
147+
Description: "image import with platform",
148+
Cleanup: func(data test.Data, helpers test.Helpers) {
149+
helpers.Anyhow("rmi", "-f", data.Identifier())
150+
},
151+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
152+
cmd := helpers.Command("import", "--platform", "linux/amd64", "-", data.Identifier())
153+
cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes()))
154+
return cmd
155+
},
156+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
157+
identifier := data.Identifier() + ":latest"
158+
return &test.Expected{
159+
Output: expect.All(
160+
func(stdout string, t tig.T) {
161+
img := nerdtest.InspectImage(helpers, identifier)
162+
assert.Equal(t, img.Architecture, "amd64")
163+
assert.Equal(t, img.Os, "linux")
164+
},
165+
),
166+
}
167+
},
168+
},
169+
{
170+
Description: "image import from URL",
171+
Cleanup: func(data test.Data, helpers test.Helpers) {
172+
if stopServer != nil {
173+
stopServer()
174+
stopServer = nil
175+
}
176+
helpers.Anyhow("rmi", "-f", data.Identifier())
177+
},
178+
Setup: func(data test.Data, helpers test.Helpers) {
179+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
180+
w.Header().Set("Content-Type", "application/x-tar")
181+
_, _ = w.Write(minimalRootfsTar(t).Bytes())
182+
})
183+
url, stop, err := nerdtest.StartHTTPServer(handler)
184+
assert.NilError(t, err)
185+
stopServer = stop
186+
data.Labels().Set("url", url)
187+
},
188+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
189+
return helpers.Command("import", data.Labels().Get("url"), data.Identifier())
190+
},
191+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
192+
identifier := data.Identifier()
193+
return &test.Expected{
194+
Output: expect.All(
195+
func(stdout string, t tig.T) {
196+
imgs := helpers.Capture("images")
197+
assert.Assert(t, strings.Contains(imgs, identifier))
198+
},
199+
),
200+
}
201+
},
202+
},
203+
}
204+
testCase.Run(t)
205+
}

cmd/nerdctl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ Config file ($NERDCTL_TOML): %s
304304
image.PushCommand(),
305305
image.LoadCommand(),
306306
image.SaveCommand(),
307+
image.ImportCommand(),
307308
image.TagCommand(),
308309
image.RmiCommand(),
309310
image.HistoryCommand(),

docs/command-reference.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ It does not necessarily mean that the corresponding features are missing in cont
4444
- [:whale: nerdctl push](#whale-nerdctl-push)
4545
- [:whale: nerdctl load](#whale-nerdctl-load)
4646
- [:whale: nerdctl save](#whale-nerdctl-save)
47+
- [:whale: nerdctl import](#whale-nerdctl-import)
4748
- [:whale: nerdctl tag](#whale-nerdctl-tag)
4849
- [:whale: nerdctl rmi](#whale-nerdctl-rmi)
4950
- [:whale: nerdctl image inspect](#whale-nerdctl-image-inspect)
@@ -905,6 +906,19 @@ Flags:
905906
- :nerd_face: `--platform=(amd64|arm64|...)`: Export content for a specific platform
906907
- :nerd_face: `--all-platforms`: Export content for all platforms
907908

909+
### :whale: nerdctl import
910+
911+
Import the contents from a tarball to create a filesystem image.
912+
913+
Usage: `nerdctl import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]`
914+
915+
Flags:
916+
917+
- :whale: `-m, --message`: Set commit message for imported image
918+
- :nerd_face: `--platform=(linux/amd64|linux/arm64|...)`: Set platform for the imported image
919+
920+
Unimplemented `docker import` flags: `--change`
921+
908922
### :whale: nerdctl tag
909923

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

19201934
Image:
19211935

1922-
- `docker import`
19231936
- `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).)
19241937

19251938
Network management:

0 commit comments

Comments
 (0)