Skip to content

Commit

Permalink
Add container run from oci-archive
Browse files Browse the repository at this point in the history
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
  • Loading branch information
austinvazquez committed Oct 17, 2024
1 parent 8a6d9e4 commit 971d297
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 2 deletions.
32 changes: 32 additions & 0 deletions cmd/nerdctl/container/container_create_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (

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

"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
"github.com/containerd/nerdctl/v2/pkg/testutil"
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
"github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
Expand Down Expand Up @@ -304,3 +305,34 @@ func TestIssue2993(t *testing.T) {

testCase.Run(t)
}

func TestCreateFromOCIArchive(t *testing.T) {
testutil.RequiresBuild(t)
testutil.RegisterBuildCacheCleanup(t)

// Docker does not support creating containers from OCI archive.
testutil.DockerIncompatible(t)

base := testutil.NewBase(t)
imageName := testutil.Identifier(t)
containerName := testutil.Identifier(t)

teardown := func() {
base.Cmd("rm", containerName).Run()
base.Cmd("rmi", imageName).Run()
}
defer teardown()
teardown()

const sentinel = "test-nerdctl-create-from-oci-archive"
dockerfile := fmt.Sprintf(`FROM %s
CMD ["echo", "%s"]`, testutil.CommonImage, sentinel)

buildCtx := helpers.CreateBuildContext(t, dockerfile)
tag := fmt.Sprintf("%s:latest", imageName)
tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName)

base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK()
base.Cmd("create", "--rm", "--name", containerName, fmt.Sprintf("oci-archive://%s", tarPath)).AssertOK()
base.Cmd("start", "--attach", containerName).AssertOutContains("test-nerdctl-create-from-oci-archive")
}
28 changes: 28 additions & 0 deletions cmd/nerdctl/container/container_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,31 @@ func TestRunQuiet(t *testing.T) {

assert.Assert(t, wasQuiet(result.Combined(), sentinel), "Found %s in container run output", sentinel)
}

func TestRunFromOCIArchive(t *testing.T) {
testutil.RequiresBuild(t)
testutil.RegisterBuildCacheCleanup(t)

// Docker does not support running container images from OCI archive.
testutil.DockerIncompatible(t)

base := testutil.NewBase(t)
imageName := testutil.Identifier(t)

teardown := func() {
base.Cmd("rmi", imageName).Run()
}
defer teardown()
teardown()

const sentinel = "test-nerdctl-run-from-oci-archive"
dockerfile := fmt.Sprintf(`FROM %s
CMD ["echo", "%s"]`, testutil.CommonImage, sentinel)

buildCtx := helpers.CreateBuildContext(t, dockerfile)
tag := fmt.Sprintf("%s:latest", imageName)
tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName)

base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK()
base.Cmd("run", "--rm", fmt.Sprintf("oci-archive://%s", tarPath)).AssertOutContainsAll(fmt.Sprintf("Loaded image: %s", tag), sentinel)
}
2 changes: 2 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Run a command in a new container.
Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]`

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

Basic flags:

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

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

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.

Expand Down
32 changes: 32 additions & 0 deletions pkg/cmd/container/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import (
"github.com/containerd/nerdctl/v2/pkg/flagutil"
"github.com/containerd/nerdctl/v2/pkg/idgen"
"github.com/containerd/nerdctl/v2/pkg/imgutil"
"github.com/containerd/nerdctl/v2/pkg/imgutil/load"
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
"github.com/containerd/nerdctl/v2/pkg/ipcutil"
"github.com/containerd/nerdctl/v2/pkg/labels"
Expand Down Expand Up @@ -123,6 +124,37 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
}
opts = append(opts, platformOpts...)

if imageRef, err := referenceutil.Parse(args[0]); err == nil && imageRef.Protocol == referenceutil.OCIArchiveProtocol {
// Load and create the platform specified by the user.
// If none specified, fallback to the default platform.
platform := []string{}
if options.Platform != "" {
platform = append(platform, options.Platform)
}

images, err := load.FromOCIArchive(ctx, client, imageRef.Path, types.ImageLoadOptions{
Stdout: options.Stdout,
GOptions: options.GOptions,
Platform: platform,
AllPlatforms: false,
Quiet: options.ImagePullOpt.Quiet,
})
if err != nil {
return nil, nil, err
} else if len(images) == 0 {
// This is a regression and should not occur.
return nil, nil, errors.New("OCI archive did not contain any images")
}

image := images[0].Name
// Multi-image archive provided, default to first image found.
if len(images) != 1 {
log.L.Warnf("multi-image OCI archive provided, defaulting to image %s...", image)
}

args[0] = image
}

var ensuredImage *imgutil.EnsuredImage
if !options.Rootfs {
var platformSS []string // len: 0 or 1
Expand Down
20 changes: 20 additions & 0 deletions pkg/imgutil/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"io"
"os"
"strings"

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

// FromOCIArchive loads and unpacks images from the on-disk OCI archive.
func FromOCIArchive(ctx context.Context, client *containerd.Client, pathToOCIArchive string, options types.ImageLoadOptions) ([]images.Image, error) {
const ociArchivePrefix = "oci-archive://"
pathToOCIArchive = strings.TrimPrefix(pathToOCIArchive, ociArchivePrefix)

const separator = ":"
if strings.Contains(pathToOCIArchive, separator) {
subs := strings.Split(pathToOCIArchive, separator)
if len(subs) != 2 {
return []images.Image{}, errors.New("too many seperators found in oci-archive path")
}
pathToOCIArchive = subs[0]
}

options.Input = pathToOCIArchive

return FromArchive(ctx, client, options)
}

type readCounter struct {
io.Reader
N int
Expand Down
11 changes: 9 additions & 2 deletions pkg/referenceutil/referenceutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import (

type Protocol string

const IPFSProtocol Protocol = "ipfs"
const IPNSProtocol Protocol = "ipns"
const (
IPFSProtocol Protocol = "ipfs"
IPNSProtocol Protocol = "ipns"
OCIArchiveProtocol Protocol = "oci-archive"
)
const shortIDLength = 5

type ImageReference struct {
Expand Down Expand Up @@ -97,6 +100,10 @@ func Parse(rawRef string) (*ImageReference, error) {
} else if strings.HasPrefix(rawRef, "ipns://") {
ir.Protocol = IPNSProtocol
rawRef = rawRef[7:]
} else if strings.HasPrefix(rawRef, "oci-archive://") {
ir.Protocol = OCIArchiveProtocol
ir.Path = rawRef[14:]
return ir, nil
}
if decodedCID, err := cid.Decode(rawRef); err == nil {
ir.Protocol = IPFSProtocol
Expand Down
8 changes: 8 additions & 0 deletions pkg/referenceutil/referenceutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,14 @@ func TestReferenceUtil(t *testing.T) {
Tag: "latest",
ExplicitTag: "",
},
"oci-archive:///tmp/build/saved-image.tar": {
Protocol: "oci-archive",
Path: "/tmp/build/saved-image.tar",
Error: "",
String: "/tmp/build/saved-image.tar",
Suggested: "oci-archive-/tmp/-abcde",
FamiliarName: "/tmp/build/saved-image.tar",
},
}

for k, v := range needles {
Expand Down

0 comments on commit 971d297

Please sign in to comment.