Skip to content

Commit 0be0b06

Browse files
committed
multi-platform
See `docs/multi-platform.md` - [X] image convert (not new in new this commit) - [X] image inspect - [X] rmi - [X] pull - [X] push - [X] load - [X] save - [X] run - [X] commit - [X] build - [X] compose - [X] docs - [X] tests Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
1 parent a68bd05 commit 0be0b06

33 files changed

Lines changed: 851 additions & 160 deletions

File tree

.github/workflows/test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ jobs:
7070
sudo apt-get purge -y snapd
7171
sudo losetup -Dv
7272
sudo losetup -lv
73+
- name: "Register QEMU (tonistiigi/binfmt)"
74+
run: docker run --privileged --rm tonistiigi/binfmt --install all
7375
- name: "Run integration tests"
7476
run: docker run -t --rm --privileged test-integration go test -v ./cmd/nerdctl/... -args -test.kill-daemon
7577

@@ -85,6 +87,8 @@ jobs:
8587
- uses: actions/checkout@v2
8688
with:
8789
fetch-depth: 1
90+
- name: "Register QEMU (tonistiigi/binfmt)"
91+
run: docker run --privileged --rm tonistiigi/binfmt --install all
8892
- name: "Prepare (network driver=slirp4netns, port driver=builtin)"
8993
run: DOCKER_BUILDKIT=1 docker build -t test-integration-rootless --target test-integration-rootless --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} .
9094
- name: "Test (network driver=slirp4netns, port driver=builtin)"
@@ -117,6 +121,8 @@ jobs:
117121
- uses: actions/checkout@v2
118122
with:
119123
fetch-depth: 1
124+
- name: "Register QEMU (tonistiigi/binfmt)"
125+
run: docker run --privileged --rm tonistiigi/binfmt --install all
120126
- name: "Ensure that the integration test suite is compatible with Docker"
121127
run: go test -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.kill-daemon
122128

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,6 @@ $ limactl start
8989
$ lima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine
9090
```
9191

92-
NOTE: ARM Mac requires installing a patched version of QEMU, see [Lima](https://github.com/AkihiroSuda/lima) documentation.
93-
9492
### FreeBSD
9593

9694
See [`./docs/freebsd.md`](docs/freebsd.md).
@@ -131,6 +129,7 @@ Minor:
131129
- Specifying a non-image rootfs: `nerdctl run -it --rootfs <ROOTFS> /bin/sh` . The CLI syntax conforms to Podman convention.
132130
- Connecting a container to multiple networks at once: `nerdctl run --net foo --net bar`
133131
- Running [FreeBSD jails](./docs/freebsd.md).
132+
- Better multi-platform support, e.g., `nerdctl pull --all-platforms IMAGE`
134133

135134
Trivial:
136135
- Inspecting raw OCI config: `nerdctl container inspect --mode=native` .
@@ -291,6 +290,9 @@ Basic flags:
291290
- Default: "missing"
292291
- :whale: `--pid=(host)`: PID namespace to use
293292

293+
Platform flags:
294+
- :whale: `--platform=(amd64|arm64|...)`: Set platform
295+
294296
Network flags:
295297
- :whale: `--net, --network=(bridge|host|none|<CNI>)`: Connect a container to a network
296298
- Default: "bridge"
@@ -620,8 +622,9 @@ Flags:
620622
- :whale: `-q, --quiet`: Suppress the build output and print image ID on success
621623
- :whale: `--cache-from=CACHE`: External cache sources (eg. user/app:cache, type=local,src=path/to/dir) (compatible with `docker buildx build`)
622624
- :whale: `--cache-to=CACHE`: Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir) (compatible with `docker buildx build`)
625+
- :whale: `--platform=(amd64|arm64|...)`: Set target platform for build (compatible with `docker buildx build`)
623626

624-
Unimplemented `docker build` flags: `--add-host`, `--iidfile`, `--label`, `--network`, `--platform`, `--squash`
627+
Unimplemented `docker build` flags: `--add-host`, `--iidfile`, `--label`, `--network`, `--squash`
625628

626629
### :whale: nerdctl commit
627630
Create a new image from a container's changes
@@ -656,13 +659,22 @@ Pull an image from a registry.
656659

657660
Usage: `nerdctl pull [OPTIONS] NAME[:TAG|@DIGEST]`
658661

659-
Unimplemented `docker pull` flags: `--all-tags`, `--disable-content-trust` (default true), `--platform`, `--quiet`
662+
Flags:
663+
- :whale: `--platform=(amd64|arm64|...)`: Pull content for a specific platform
664+
- :nerd_face: Unlike Docker, this flag can be specified multiple times (`--platform=amd64 --platform=arm64`)
665+
- :nerd_face: `--all-platforms`: Pull content for all platforms
666+
667+
Unimplemented `docker pull` flags: `--all-tags`, `--disable-content-trust` (default true), `--quiet`
660668

661669
### :whale: nerdctl push
662670
Push an image to a registry.
663671

664672
Usage: `nerdctl push [OPTIONS] NAME[:TAG]`
665673

674+
Flags:
675+
- :nerd_face: `--platform=(amd64|arm64|...)`: Push content for a specific platform
676+
- :nerd_face: `--all-platforms`: Push content for all platforms
677+
666678
Unimplemented `docker push` flags: `--all-tags`, `--disable-content-trust` (default true), `--quiet`
667679

668680
### :whale: nerdctl load
@@ -674,6 +686,8 @@ Usage: `nerdctl load [OPTIONS]`
674686

675687
Flags:
676688
- :whale: `-i, --input`: Read from tar archive file, instead of STDIN
689+
- :nerd_face: `--platform=(amd64|arm64|...)`: Import content for a specific platform
690+
- :nerd_face: `--all-platforms`: Import content for all platforms
677691

678692
Unimplemented `docker load` flags: `--quiet`
679693

@@ -686,6 +700,8 @@ Usage: `nerdctl save [OPTIONS] IMAGE [IMAGE...]`
686700

687701
Flags:
688702
- :whale: `-o, --output`: Write to a file, instead of STDOUT
703+
- :nerd_face: `--platform=(amd64|arm64|...)`: Export content for a specific platform
704+
- :nerd_face: `--all-platforms`: Export content for all platforms
689705

690706
### :whale: nerdctl tag
691707
Create a tag TARGET\_IMAGE that refers to SOURCE\_IMAGE.
@@ -707,6 +723,7 @@ Usage: `nerdctl image inspect [OPTIONS] NAME|ID [NAME|ID...]`
707723
Flags:
708724
- :nerd_face: `--mode=(dockercompat|native)`: Inspection mode. "native" produces more information.
709725
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
726+
- :nerd_face: `--platform=(amd64|arm64|...)`: Inspect a specific platform
710727

711728
### :nerd_face: nerdctl image convert
712729
Convert an image format.
@@ -1025,4 +1042,5 @@ Others:
10251042
- [`./docs/stargz.md`](./docs/stargz.md): Lazy-pulling using Stargz Snapshotter
10261043
- [`./docs/ocicrypt.md`](./docs/ocicrypt.md): Running encrypted images
10271044
- [`./docs/freebsd.md`](./docs/freebsd.md): Running FreeBSD jails
1045+
- [`./docs/multi-platform.md`](./docs/multi-platform.md): Multi-platform mode
10281046
- [`./docs/experimental.md`](./docs/experimental.md): Experimental features

cmd/nerdctl/build.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"github.com/containerd/nerdctl/pkg/buildkitutil"
2929
"github.com/containerd/nerdctl/pkg/defaults"
30+
"github.com/containerd/nerdctl/pkg/platformutil"
3031
"github.com/containerd/nerdctl/pkg/strutil"
3132
"github.com/pkg/errors"
3233
"github.com/sirupsen/logrus"
@@ -63,10 +64,21 @@ func newBuildCommand() *cobra.Command {
6364
buildCommand.Flags().StringSlice("cache-from", nil, "External cache sources (eg. user/app:cache, type=local,src=path/to/dir)")
6465
buildCommand.Flags().StringSlice("cache-to", nil, "Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir)")
6566

67+
// #region platform flags
68+
buildCommand.Flags().StringSlice("platform", []string{}, "Set target platform for build (e.g., \"amd64\", \"arm64\")")
69+
buildCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
70+
// #endregion
71+
6672
return buildCommand
6773
}
6874

6975
func buildAction(cmd *cobra.Command, args []string) error {
76+
platform, err := cmd.Flags().GetStringSlice("platform")
77+
if err != nil {
78+
return err
79+
}
80+
platform = strutil.DedupeStrSlice(platform)
81+
7082
buildkitHost, err := cmd.Flags().GetString("buildkit-host")
7183
if err != nil {
7284
return err
@@ -75,7 +87,7 @@ func buildAction(cmd *cobra.Command, args []string) error {
7587
return err
7688
}
7789

78-
buildctlBinary, buildctlArgs, needsLoading, cleanup, err := generateBuildctlArgs(cmd, args)
90+
buildctlBinary, buildctlArgs, needsLoading, cleanup, err := generateBuildctlArgs(cmd, platform, args)
7991
if err != nil {
8092
return err
8193
}
@@ -110,7 +122,11 @@ func buildAction(cmd *cobra.Command, args []string) error {
110122
}
111123

112124
if needsLoading {
113-
if err = loadImage(buildctlStdout, cmd, args, false, quiet); err != nil {
125+
platMC, err := platformutil.NewMatchComparer(false, platform)
126+
if err != nil {
127+
return err
128+
}
129+
if err = loadImage(buildctlStdout, cmd, args, platMC, quiet); err != nil {
114130
return err
115131
}
116132
}
@@ -122,7 +138,7 @@ func buildAction(cmd *cobra.Command, args []string) error {
122138
return nil
123139
}
124140

125-
func generateBuildctlArgs(cmd *cobra.Command, args []string) (string, []string, bool, func(), error) {
141+
func generateBuildctlArgs(cmd *cobra.Command, platform, args []string) (string, []string, bool, func(), error) {
126142
var needsLoading bool
127143
if len(args) < 1 {
128144
return "", nil, false, nil, errors.New("context needs to be specified")
@@ -143,6 +159,11 @@ func generateBuildctlArgs(cmd *cobra.Command, args []string) (string, []string,
143159
}
144160
if output == "" {
145161
output = "type=docker"
162+
if len(platform) > 1 {
163+
// For avoiding `error: failed to solve: docker exporter does not currently support exporting manifest lists`
164+
// TODO: consider using type=oci for single-platform build too
165+
output = "type=oci"
166+
}
146167
needsLoading = true
147168
}
148169
tagValue, err := cmd.Flags().GetStringSlice("tag")
@@ -214,6 +235,10 @@ func generateBuildctlArgs(cmd *cobra.Command, args []string) (string, []string,
214235
buildctlArgs = append(buildctlArgs, "--opt=target="+target)
215236
}
216237

238+
if len(platform) > 0 {
239+
buildctlArgs = append(buildctlArgs, "--opt=platform="+strings.Join(platform, ","))
240+
}
241+
217242
buildArgsValue, err := cmd.Flags().GetStringSlice("build-arg")
218243
if err != nil {
219244
return "", nil, false, cleanup, err

cmd/nerdctl/commit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func commitAction(cmd *cobra.Command, args []string) error {
6363
if found.MatchCount > 1 {
6464
return errors.Errorf("ambiguous ID %q", found.Req)
6565
}
66-
imageID, err := commit.Commit(ctx, client, found.Container.ID(), opts)
66+
imageID, err := commit.Commit(ctx, client, found.Container, opts)
6767
if err != nil {
6868
return err
6969
}

cmd/nerdctl/completion.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,17 @@ func shellCompleteVolumeNames(cmd *cobra.Command) ([]string, cobra.ShellCompDire
138138
}
139139
return candidates, cobra.ShellCompDirectiveNoFileComp
140140
}
141+
142+
func shellCompletePlatforms(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
143+
candidates := []string{
144+
"amd64",
145+
"arm64",
146+
"riscv64",
147+
"ppc64le",
148+
"s390x",
149+
"386",
150+
"arm", // alias of "linux/arm/v7"
151+
"linux/arm/v6", // "arm/v6" is invalid (interpreted as OS="arm", Arch="v7")
152+
}
153+
return candidates, cobra.ShellCompDirectiveNoFileComp
154+
}

cmd/nerdctl/compose.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ import (
2222
composecli "github.com/compose-spec/compose-go/cli"
2323
"github.com/containerd/containerd"
2424
"github.com/containerd/containerd/errdefs"
25+
"github.com/containerd/containerd/platforms"
2526
refdocker "github.com/containerd/containerd/reference/docker"
2627
"github.com/containerd/nerdctl/pkg/composer"
2728
"github.com/containerd/nerdctl/pkg/imgutil"
2829
"github.com/containerd/nerdctl/pkg/netutil"
30+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2931
"github.com/pkg/errors"
3032
"github.com/spf13/cobra"
3133
)
@@ -155,9 +157,17 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo
155157
return true, nil
156158
}
157159

158-
o.EnsureImage = func(ctx context.Context, imageName, pullMode string) error {
160+
o.EnsureImage = func(ctx context.Context, imageName, pullMode, platform string) error {
161+
ocispecPlatforms := []ocispec.Platform{platforms.DefaultSpec()}
162+
if platform != "" {
163+
parsed, err := platforms.Parse(platform)
164+
if err != nil {
165+
return err
166+
}
167+
ocispecPlatforms = []ocispec.Platform{parsed} // no append
168+
}
159169
_, imgErr := imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), snapshotter, imageName,
160-
pullMode, insecure)
170+
pullMode, insecure, ocispecPlatforms)
161171
return imgErr
162172
}
163173

cmd/nerdctl/image_convert.go

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ import (
2424

2525
"github.com/containerd/containerd/images/converter"
2626
"github.com/containerd/containerd/images/converter/uncompress"
27-
"github.com/containerd/containerd/platforms"
2827
refdocker "github.com/containerd/containerd/reference/docker"
29-
"github.com/containerd/nerdctl/pkg/strutil"
28+
"github.com/containerd/nerdctl/pkg/platformutil"
3029
"github.com/containerd/stargz-snapshotter/estargz"
3130
estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
3231
"github.com/containerd/stargz-snapshotter/recorder"
33-
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3432
"github.com/pkg/errors"
3533
"github.com/sirupsen/logrus"
3634
"github.com/spf13/cobra"
@@ -71,6 +69,7 @@ func newImageConvertCommand() *cobra.Command {
7169

7270
// #region platform flags
7371
imageConvertCommand.Flags().StringSlice("platform", []string{}, "Convert content for a specific platform")
72+
imageConvertCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
7473
imageConvertCommand.Flags().Bool("all-platforms", false, "Convert content for all platforms")
7574
// #endregion
7675

@@ -107,22 +106,11 @@ func imageConvertAction(cmd *cobra.Command, args []string) error {
107106
if err != nil {
108107
return err
109108
}
110-
111-
if !allPlatforms {
112-
if pss := strutil.DedupeStrSlice(platform); len(pss) > 0 {
113-
var all []ocispec.Platform
114-
for _, ps := range pss {
115-
p, err := platforms.Parse(ps)
116-
if err != nil {
117-
return errors.Wrapf(err, "invalid platform %q", ps)
118-
}
119-
all = append(all, p)
120-
}
121-
convertOpts = append(convertOpts, converter.WithPlatform(platforms.Ordered(all...)))
122-
} else {
123-
convertOpts = append(convertOpts, converter.WithPlatform(platforms.DefaultStrict()))
124-
}
109+
platMC, err := platformutil.NewMatchComparer(allPlatforms, platform)
110+
if err != nil {
111+
return err
125112
}
113+
convertOpts = append(convertOpts, converter.WithPlatform(platMC))
126114

127115
estargz, err := cmd.Flags().GetBool("estargz")
128116
if err != nil {

cmd/nerdctl/image_inspect.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"text/template"
2525
"time"
2626

27+
"github.com/containerd/containerd"
28+
"github.com/containerd/containerd/platforms"
2729
"github.com/containerd/nerdctl/pkg/idutil/imagewalker"
2830
"github.com/containerd/nerdctl/pkg/imageinspector"
2931
"github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
@@ -49,6 +51,12 @@ func newImageInspectCommand() *cobra.Command {
4951
return []string{"dockercompat", "native"}, cobra.ShellCompDirectiveNoFileComp
5052
})
5153
imageInspectCommand.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'")
54+
55+
// #region platform flags
56+
imageInspectCommand.Flags().String("platform", "", "Inspect a specific platform") // not a slice, and there is no --all-platforms
57+
imageInspectCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
58+
// #endregion
59+
5260
return imageInspectCommand
5361
}
5462

@@ -57,7 +65,20 @@ func imageInspectAction(cmd *cobra.Command, args []string) error {
5765
return errors.Errorf("requires at least 1 argument")
5866
}
5967

60-
client, ctx, cancel, err := newClient(cmd)
68+
var clientOpts []containerd.ClientOpt
69+
platform, err := cmd.Flags().GetString("platform")
70+
if err != nil {
71+
return err
72+
}
73+
if platform != "" {
74+
platformParsed, err := platforms.Parse(platform)
75+
if err != nil {
76+
return err
77+
}
78+
platformM := platforms.Only(platformParsed)
79+
clientOpts = append(clientOpts, containerd.WithDefaultPlatform(platformM))
80+
}
81+
client, ctx, cancel, err := newClient(cmd, clientOpts...)
6182
if err != nil {
6283
return err
6384
}

0 commit comments

Comments
 (0)