Skip to content

Commit 0612a1d

Browse files
authored
Merge pull request #3537 from austinvazquez/feat-run-oci-archive
Add support for running container from OCI archive
2 parents 07cb00b + a4b2959 commit 0612a1d

File tree

9 files changed

+260
-114
lines changed

9 files changed

+260
-114
lines changed

cmd/nerdctl/container/container_create_linux_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
"github.com/containerd/containerd/v2/defaults"
3131

32+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
3233
"github.com/containerd/nerdctl/v2/pkg/testutil"
3334
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
3435
"github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
@@ -306,3 +307,34 @@ func TestIssue2993(t *testing.T) {
306307

307308
testCase.Run(t)
308309
}
310+
311+
func TestCreateFromOCIArchive(t *testing.T) {
312+
testutil.RequiresBuild(t)
313+
testutil.RegisterBuildCacheCleanup(t)
314+
315+
// Docker does not support creating containers from OCI archive.
316+
testutil.DockerIncompatible(t)
317+
318+
base := testutil.NewBase(t)
319+
imageName := testutil.Identifier(t)
320+
containerName := testutil.Identifier(t)
321+
322+
teardown := func() {
323+
base.Cmd("rm", "-f", containerName).Run()
324+
base.Cmd("rmi", "-f", imageName).Run()
325+
}
326+
defer teardown()
327+
teardown()
328+
329+
const sentinel = "test-nerdctl-create-from-oci-archive"
330+
dockerfile := fmt.Sprintf(`FROM %s
331+
CMD ["echo", "%s"]`, testutil.CommonImage, sentinel)
332+
333+
buildCtx := helpers.CreateBuildContext(t, dockerfile)
334+
tag := fmt.Sprintf("%s:latest", imageName)
335+
tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName)
336+
337+
base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK()
338+
base.Cmd("create", "--rm", "--name", containerName, fmt.Sprintf("oci-archive://%s", tarPath)).AssertOK()
339+
base.Cmd("start", "--attach", containerName).AssertOutContains("test-nerdctl-create-from-oci-archive")
340+
}

cmd/nerdctl/container/container_run_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,3 +658,31 @@ func TestRunQuiet(t *testing.T) {
658658

659659
assert.Assert(t, wasQuiet(result.Combined(), sentinel), "Found %s in container run output", sentinel)
660660
}
661+
662+
func TestRunFromOCIArchive(t *testing.T) {
663+
testutil.RequiresBuild(t)
664+
testutil.RegisterBuildCacheCleanup(t)
665+
666+
// Docker does not support running container images from OCI archive.
667+
testutil.DockerIncompatible(t)
668+
669+
base := testutil.NewBase(t)
670+
imageName := testutil.Identifier(t)
671+
672+
teardown := func() {
673+
base.Cmd("rmi", "-f", imageName).Run()
674+
}
675+
defer teardown()
676+
teardown()
677+
678+
const sentinel = "test-nerdctl-run-from-oci-archive"
679+
dockerfile := fmt.Sprintf(`FROM %s
680+
CMD ["echo", "%s"]`, testutil.CommonImage, sentinel)
681+
682+
buildCtx := helpers.CreateBuildContext(t, dockerfile)
683+
tag := fmt.Sprintf("%s:latest", imageName)
684+
tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName)
685+
686+
base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK()
687+
base.Cmd("run", "--rm", fmt.Sprintf("oci-archive://%s", tarPath)).AssertOutContainsAll(fmt.Sprintf("Loaded image: %s", tag), sentinel)
688+
}

cmd/nerdctl/image/image_load.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
2424
"github.com/containerd/nerdctl/v2/pkg/api/types"
2525
"github.com/containerd/nerdctl/v2/pkg/clientutil"
26-
"github.com/containerd/nerdctl/v2/pkg/cmd/image"
26+
"github.com/containerd/nerdctl/v2/pkg/imgutil/load"
2727
)
2828

2929
func NewLoadCommand() *cobra.Command {
@@ -94,5 +94,6 @@ func loadAction(cmd *cobra.Command, _ []string) error {
9494
}
9595
defer cancel()
9696

97-
return image.Load(ctx, client, options)
97+
_, err = load.FromArchive(ctx, client, options)
98+
return err
9899
}

docs/command-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ Run a command in a new container.
134134
Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]`
135135

136136
:nerd_face: `ipfs://` prefix can be used for `IMAGE` to pull it from IPFS. See [`ipfs.md`](./ipfs.md) for details.
137+
:nerd_face: `oci-archive://` prefix can be used for `IMAGE` to specify a local file system path to an OCI formatted tarball.
137138

138139
Basic flags:
139140

@@ -423,6 +424,7 @@ Create a new container.
423424
Usage: `nerdctl create [OPTIONS] IMAGE [COMMAND] [ARG...]`
424425

425426
:nerd_face: `ipfs://` prefix can be used for `IMAGE` to pull it from IPFS. See [`ipfs.md`](./ipfs.md) for details.
427+
:nerd_face: `oci-archive://` prefix can be used for `IMAGE` to specify a local file system path to an OCI formatted tarball.
426428

427429
The `nerdctl create` command similar to `nerdctl run -d` except the container is never started. You can then use the `nerdctl start <container_id>` command to start the container at any point.
428430

pkg/cmd/container/create.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import (
5050
"github.com/containerd/nerdctl/v2/pkg/flagutil"
5151
"github.com/containerd/nerdctl/v2/pkg/idgen"
5252
"github.com/containerd/nerdctl/v2/pkg/imgutil"
53+
"github.com/containerd/nerdctl/v2/pkg/imgutil/load"
5354
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
5455
"github.com/containerd/nerdctl/v2/pkg/ipcutil"
5556
"github.com/containerd/nerdctl/v2/pkg/labels"
@@ -123,6 +124,39 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
123124
}
124125
opts = append(opts, platformOpts...)
125126

127+
if _, err := referenceutil.Parse(args[0]); errors.Is(err, referenceutil.ErrLoadOCIArchiveRequired) {
128+
imageRef := args[0]
129+
130+
// Load and create the platform specified by the user.
131+
// If none specified, fallback to the default platform.
132+
platform := []string{}
133+
if options.Platform != "" {
134+
platform = append(platform, options.Platform)
135+
}
136+
137+
images, err := load.FromOCIArchive(ctx, client, imageRef, types.ImageLoadOptions{
138+
Stdout: options.Stdout,
139+
GOptions: options.GOptions,
140+
Platform: platform,
141+
AllPlatforms: false,
142+
Quiet: options.ImagePullOpt.Quiet,
143+
})
144+
if err != nil {
145+
return nil, nil, err
146+
} else if len(images) == 0 {
147+
// This is a regression and should not occur.
148+
return nil, nil, errors.New("OCI archive did not contain any images")
149+
}
150+
151+
image := images[0].Name
152+
// Multiple images loaded from the provided archive. Default to the first image found.
153+
if len(images) != 1 {
154+
log.L.Warnf("multiple images are found for the platform, defaulting to image %s...", image)
155+
}
156+
157+
args[0] = image
158+
}
159+
126160
var ensuredImage *imgutil.EnsuredImage
127161
if !options.Rootfs {
128162
var platformSS []string // len: 0 or 1

pkg/cmd/image/load.go

Lines changed: 0 additions & 112 deletions
This file was deleted.

0 commit comments

Comments
 (0)