Skip to content

Commit 51d4015

Browse files
committed
Add load subcommand
1 parent 91d49f9 commit 51d4015

File tree

6 files changed

+274
-0
lines changed

6 files changed

+274
-0
lines changed

client/load.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
8+
"github.com/containerd/containerd/content"
9+
"github.com/containerd/containerd/errdefs"
10+
"github.com/containerd/containerd/images"
11+
"github.com/containerd/containerd/images/archive"
12+
"github.com/docker/distribution/reference"
13+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
14+
15+
"github.com/pkg/errors"
16+
"github.com/sirupsen/logrus"
17+
)
18+
19+
// LoadProgress receives informational updates about the progress of image loading.
20+
type LoadProgress interface {
21+
// LoadImageCreated indicates that the given image has been created.
22+
LoadImageCreated(images.Image) error
23+
24+
// LoadImageUpdated indicates that the given image has been updated.
25+
LoadImageUpdated(images.Image) error
26+
27+
// LoadImageReplaced indicates that an image has been replaced, leaving the old image without a name.
28+
LoadImageReplaced(before, after images.Image) error
29+
}
30+
31+
// LoadImage imports an image into the image store.
32+
func (c *Client) LoadImage(ctx context.Context, reader io.Reader, mon LoadProgress) error {
33+
// Create the worker opts.
34+
opt, err := c.createWorkerOpt(false)
35+
if err != nil {
36+
return errors.Wrap(err, "creating worker opt failed")
37+
}
38+
39+
if opt.ImageStore == nil {
40+
return errors.New("image store is nil")
41+
}
42+
43+
logrus.Debug("Importing index")
44+
index, err := importIndex(ctx, opt.ContentStore, reader)
45+
if err != nil {
46+
return err
47+
}
48+
49+
logrus.Debug("Extracting manifests")
50+
manifests := extractManifests(index)
51+
52+
for _, imageSkel := range manifests {
53+
err := load(ctx, opt.ImageStore, imageSkel, mon)
54+
if err != nil {
55+
return nil
56+
}
57+
}
58+
59+
return nil
60+
}
61+
62+
func importIndex(ctx context.Context, store content.Store, reader io.Reader) (*ocispec.Index, error) {
63+
d, err := archive.ImportIndex(ctx, store, reader)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
indexBytes, err := content.ReadBlob(ctx, store, d)
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
var index ocispec.Index
74+
err = json.Unmarshal(indexBytes, &index)
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
return &index, nil
80+
}
81+
82+
func extractManifests(index *ocispec.Index) []images.Image {
83+
var result []images.Image
84+
for _, m := range index.Manifests {
85+
switch m.MediaType {
86+
case images.MediaTypeDockerSchema2Manifest:
87+
if name, ok := m.Annotations[images.AnnotationImageName]; ok {
88+
if ref, ok := m.Annotations[ocispec.AnnotationRefName]; ok {
89+
if normalized, err := reference.ParseNormalizedNamed(name); err == nil {
90+
if normalizedWithTag, err := reference.WithTag(normalized, ref); err == nil {
91+
if normalized == normalizedWithTag {
92+
result = append(result, images.Image{
93+
Name: normalized.String(),
94+
Target: m,
95+
})
96+
continue
97+
}
98+
}
99+
}
100+
}
101+
}
102+
logrus.Debugf("Failed to extract image info from manifest: %v", m)
103+
}
104+
}
105+
106+
return result
107+
}
108+
109+
func load(ctx context.Context, store images.Store, imageSkel images.Image, mon LoadProgress) error {
110+
image, err := store.Get(ctx, imageSkel.Name)
111+
112+
if errors.Cause(err) == errdefs.ErrNotFound {
113+
image, err = store.Create(ctx, imageSkel)
114+
if err != nil {
115+
return err
116+
}
117+
return mon.LoadImageCreated(image)
118+
}
119+
120+
if err != nil {
121+
return err
122+
}
123+
124+
updated, err := store.Update(ctx, imageSkel)
125+
if err != nil {
126+
return err
127+
}
128+
129+
if image.Target.Digest == updated.Target.Digest {
130+
return mon.LoadImageUpdated(updated)
131+
}
132+
133+
image.Name = ""
134+
return mon.LoadImageReplaced(image, updated)
135+
}

load.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
9+
"github.com/spf13/cobra"
10+
11+
"github.com/containerd/containerd/images"
12+
"github.com/containerd/containerd/namespaces"
13+
"github.com/genuinetools/img/client"
14+
"github.com/moby/buildkit/identity"
15+
"github.com/moby/buildkit/session"
16+
)
17+
18+
// TODO(AkihiroSuda): support OCI archive
19+
const loadUsageShortHelp = `Load an image from a tar archive or STDIN.`
20+
const loadUsageLongHelp = `Load an image from a tar archive or STDIN.`
21+
22+
func newLoadCommand() *cobra.Command {
23+
24+
load := &loadCommand{}
25+
26+
cmd := &cobra.Command{
27+
Use: "load [OPTIONS]",
28+
DisableFlagsInUseLine: true,
29+
SilenceUsage: true,
30+
Short: loadUsageShortHelp,
31+
Long: loadUsageLongHelp,
32+
// Args: load.ValidateArgs,
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
return load.Run(args)
35+
},
36+
}
37+
38+
fs := cmd.Flags()
39+
40+
fs.StringVarP(&load.input, "input", "i", "", "Read from tar archive file, instead of STDIN")
41+
fs.BoolVarP(&load.quiet, "quiet", "q", false, "Suppress the load output")
42+
43+
return cmd
44+
}
45+
46+
type loadCommand struct {
47+
input string
48+
quiet bool
49+
}
50+
51+
func (cmd *loadCommand) Run(args []string) (err error) {
52+
reexec()
53+
54+
// Create the context.
55+
id := identity.NewID()
56+
ctx := session.NewContext(context.Background(), id)
57+
ctx = namespaces.WithNamespace(ctx, "buildkit")
58+
59+
// Create the client.
60+
c, err := client.New(stateDir, backend, nil)
61+
if err != nil {
62+
return err
63+
}
64+
defer c.Close()
65+
66+
// Create the reader.
67+
reader, err := cmd.reader()
68+
if err != nil {
69+
return err
70+
}
71+
defer reader.Close()
72+
73+
// Load images.
74+
return c.LoadImage(ctx, reader, cmd)
75+
}
76+
77+
func (cmd *loadCommand) reader() (io.ReadCloser, error) {
78+
if cmd.input != "" {
79+
return os.Open(cmd.input)
80+
}
81+
82+
return os.Stdin, nil
83+
}
84+
85+
func (cmd *loadCommand) LoadImageCreated(image images.Image) error {
86+
if cmd.quiet {
87+
return nil
88+
}
89+
_, err := fmt.Printf("Loaded image: %s\n", image.Name)
90+
return err
91+
}
92+
93+
func (cmd *loadCommand) LoadImageUpdated(image images.Image) error {
94+
return cmd.LoadImageCreated(image)
95+
}
96+
97+
func (cmd *loadCommand) LoadImageReplaced(before, after images.Image) error {
98+
if !cmd.quiet {
99+
_, err := fmt.Printf(
100+
"The image %s already exists, leaving the old one with ID %s orphaned\n",
101+
after.Name, before.Target.Digest)
102+
if err != nil {
103+
return err
104+
}
105+
}
106+
107+
return cmd.LoadImageCreated(after)
108+
}

load_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestLoadImage(t *testing.T) {
8+
output := run(t, "load", "-i", "testdata/img-load/oci.tar")
9+
if output != "Loaded image: docker.io/library/dummy:latest\n" {
10+
t.Fatalf("Unexpected output: %s", output)
11+
}
12+
13+
output = run(t, "load", "-i", "testdata/img-load/oci.tar")
14+
if output != "Loaded image: docker.io/library/dummy:latest\n" {
15+
t.Fatalf("Unexpected output: %s", output)
16+
}
17+
18+
output = run(t, "load", "-i", "testdata/img-load/docker.tar")
19+
expected := `The image docker.io/library/dummy:latest already exists, leaving the old one with ID sha256:e08488191147b6fc575452dfac3238721aa5b86d2545a9edaa1c1d88632b2233 orphaned
20+
Loaded image: docker.io/library/dummy:latest
21+
`
22+
if output != expected {
23+
t.Fatalf("Unexpected output: %s", output)
24+
}
25+
26+
output = run(t, "load", "-i", "testdata/img-load/docker.tar")
27+
if output != "Loaded image: docker.io/library/dummy:latest\n" {
28+
t.Fatalf("Unexpected output: %s", output)
29+
}
30+
}

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func main() {
7575
newDiskUsageCommand(),
7676
newInspectCommand(),
7777
newListCommand(),
78+
newLoadCommand(),
7879
newLoginCommand(),
7980
newLogoutCommand(),
8081
newPruneCommand(),

testdata/img-load/docker.tar

10.5 KB
Binary file not shown.

testdata/img-load/oci.tar

9 KB
Binary file not shown.

0 commit comments

Comments
 (0)