Skip to content

Commit d4a5e21

Browse files
committed
Add container run from oci-archive
Signed-off-by: Austin Vazquez <[email protected]>
1 parent f68ca0e commit d4a5e21

File tree

5 files changed

+114
-0
lines changed

5 files changed

+114
-0
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"
@@ -304,3 +305,34 @@ func TestIssue2993(t *testing.T) {
304305

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

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", 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+
}

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

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

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

426428
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.
427429

pkg/cmd/container/create.go

Lines changed: 32 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,37 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
123124
}
124125
opts = append(opts, platformOpts...)
125126

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

pkg/imgutil/load/load.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"io"
2424
"os"
25+
"strings"
2526

2627
containerd "github.com/containerd/containerd/v2/client"
2728
"github.com/containerd/containerd/v2/core/images"
@@ -76,6 +77,25 @@ func FromArchive(ctx context.Context, client *containerd.Client, options types.I
7677
return unpackedImages, nil
7778
}
7879

80+
// FromOCIArchive loads and unpacks images from the on-disk OCI archive.
81+
func FromOCIArchive(ctx context.Context, client *containerd.Client, pathToOCIArchive string, options types.ImageLoadOptions) ([]images.Image, error) {
82+
const ociArchivePrefix = "oci-archive://"
83+
pathToOCIArchive = strings.TrimPrefix(pathToOCIArchive, ociArchivePrefix)
84+
85+
const separator = ":"
86+
if strings.Contains(pathToOCIArchive, separator) {
87+
subs := strings.Split(pathToOCIArchive, separator)
88+
if len(subs) != 2 {
89+
return []images.Image{}, errors.New("too many seperators found in oci-archive path")
90+
}
91+
pathToOCIArchive = subs[0]
92+
}
93+
94+
options.Input = pathToOCIArchive
95+
96+
return FromArchive(ctx, client, options)
97+
}
98+
7999
type readCounter struct {
80100
io.Reader
81101
N int

0 commit comments

Comments
 (0)