Skip to content

Commit

Permalink
feature: add pouch save functionality
Browse files Browse the repository at this point in the history
Signed-off-by: xiechengsheng <XIE1995@whut.edu.cn>
  • Loading branch information
xiechengsheng committed Jul 12, 2018
1 parent c83ff22 commit 6f1e362
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 23 deletions.
21 changes: 21 additions & 0 deletions apis/server/image_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -151,3 +152,23 @@ func (s *Server) loadImage(ctx context.Context, rw http.ResponseWriter, req *htt
rw.WriteHeader(http.StatusOK)
return nil
}

// saveImage saves an image by http tar stream.
func (s *Server) saveImage(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
imageName := req.FormValue("name")

rw.Header().Set("Content-Type", "application/x-tar")

r, err := s.ImageMgr.SaveImage(ctx, imageName)
if err != nil {
return err
}
defer r.Close()

output := newWriteFlusher(rw)
if _, err := io.Copy(output, r); err != nil {
return err
}

return nil
}
1 change: 1 addition & 0 deletions apis/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func initRoute(s *Server) http.Handler {
s.addRoute(r, http.MethodGet, "/images/{name:.*}/json", s.getImage)
s.addRoute(r, http.MethodPost, "/images/{name:.*}/tag", s.postImageTag)
s.addRoute(r, http.MethodPost, "/images/load", withCancelHandler(s.loadImage))
s.addRoute(r, http.MethodGet, "/images/save", withCancelHandler(s.saveImage))

// volume
s.addRoute(r, http.MethodGet, "/volumes", s.listVolume)
Expand Down
23 changes: 23 additions & 0 deletions apis/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,29 @@ paths:
description: "set the image name for the tar stream, default unknown/unknown"
type: "string"

/images/save:
get:
summary: "Save image"
description: |
Save an image by oci.v1 format tar stream.
produces:
- application/x-tar
responses:
200:
description: "no error"
schema:
type: "string"
format: "binary"
404:
$ref: "#/responses/404ErrorResponse"
500:
$ref: "#/responses/500ErrorResponse"
parameters:
- name: "name"
in: "query"
description: "Image name which is to be saved"
type: "string"

/images/{imageid}/json:
get:
summary: "Inspect a image"
Expand Down
1 change: 1 addition & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func main() {
cli.AddCommand(base, &NetworkCommand{})
cli.AddCommand(base, &TagCommand{})
cli.AddCommand(base, &LoadCommand{})
cli.AddCommand(base, &SaveCommand{})

cli.AddCommand(base, &InspectCommand{})
cli.AddCommand(base, &RenameCommand{})
Expand Down
77 changes: 77 additions & 0 deletions cli/save.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"context"
"io"
"os"

"github.com/spf13/cobra"
)

// saveDescription is used to describe save command in detail and auto generate command doc.
var saveDescription = "save an image to a tar archive."

// SaveCommand use to implement 'save' command.
type SaveCommand struct {
baseCommand
output string
}

// Init initialize save command.
func (save *SaveCommand) Init(c *Cli) {
save.cli = c
save.cmd = &cobra.Command{
Use: "save [OPTIONS] IMAGE",
Short: "Save an image to a tar archive or STDOUT",
Long: saveDescription,
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
return save.runSave(args)
},
Example: saveExample(),
}
save.addFlags()
}

// addFlags adds flags for specific command.
func (save *SaveCommand) addFlags() {
flagSet := save.cmd.Flags()
flagSet.StringVarP(&save.output, "output", "o", "", "Save to a tar archive file, instead of STDOUT")
}

// runSave is the entry of save command.
func (save *SaveCommand) runSave(args []string) error {
ctx := context.Background()
apiClient := save.cli.Client()

r, err := apiClient.ImageSave(ctx, args[0])
if err != nil {
return err
}
defer r.Close()

out := os.Stdout
if save.output != "" {
out, err = os.Create(save.output)
if err != nil {
return nil
}
defer out.Close()
}

if _, err := io.Copy(out, r); err != nil {
return err
}
return nil
}

// saveExample shows examples in save command, and is used in auto-generated cli docs.
func saveExample() string {
return `$ pouch save -o busybox.tar busybox:latest
$ pouch load -i busybox.tar foo
$ pouch images
IMAGE ID IMAGE NAME SIZE
8c811b4aec35 registry.hub.docker.com/library/busybox:latest 710.81 KB
8c811b4aec35 foo:latest 710.81 KB
`
}
19 changes: 19 additions & 0 deletions client/image_save.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package client

import (
"context"
"io"
"net/url"
)

// ImageSave requests daemon to save an image to a tar archive.
func (client *APIClient) ImageSave(ctx context.Context, imageName string) (io.ReadCloser, error) {
q := url.Values{}
q.Set("name", imageName)

resp, err := client.get(ctx, "/images/save", q, nil)
if err != nil {
return nil, err
}
return resp.Body, nil
}
56 changes: 56 additions & 0 deletions client/image_save_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package client

import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
)

func TestImageSaveServerError(t *testing.T) {
expectedError := "Server error"

client := &APIClient{
HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, expectedError)),
}

_, err := client.ImageSave(context.Background(), "test_image_save_500")
if err == nil || !strings.Contains(err.Error(), expectedError) {
t.Fatalf("expected (%v), got (%v)", expectedError, err)
}
}

func TestImageSaveOK(t *testing.T) {
expectedImageName := "test_image_save_ok"
expectedURL := "/images/save"

httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
}

if req.Method != "GET" {
return nil, fmt.Errorf("expected GET method, got %s", req.Method)
}

if got := req.FormValue("name"); got != expectedImageName {
return nil, fmt.Errorf("expected (%s), got %s", expectedImageName, got)
}

return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
})

client := &APIClient{
HTTPCli: httpClient,
}

if _, err := client.ImageSave(context.Background(), expectedImageName); err != nil {
t.Fatal(err)
}
}
1 change: 1 addition & 0 deletions client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type ImageAPIClient interface {
ImageRemove(ctx context.Context, name string, force bool) error
ImageTag(ctx context.Context, image string, tag string) error
ImageLoad(ctx context.Context, name string, r io.Reader) error
ImageSave(ctx context.Context, imageName string) (io.ReadCloser, error)
}

// VolumeAPIClient defines methods of Volume client.
Expand Down
37 changes: 37 additions & 0 deletions ctrd/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/alibaba/pouch/apis/types"
"github.com/alibaba/pouch/pkg/jsonstream"
"github.com/alibaba/pouch/pkg/reference"

"github.com/containerd/containerd"
"github.com/containerd/containerd/errdefs"
Expand Down Expand Up @@ -65,6 +66,42 @@ func (c *Client) RemoveImage(ctx context.Context, ref string) error {
return nil
}

// SaveImage saves image to tarstream
func (c *Client) SaveImage(ctx context.Context, exporter ctrdmetaimages.Exporter, ref string) (io.ReadCloser, error) {
wrapperCli, err := c.Get(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get a containerd grpc client: %v", err)
}

image, err := c.GetImage(ctx, ref)
if err != nil {
return nil, err
}

desc := image.Target()
// add annotations in image description
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
if s, exist := desc.Annotations[ocispec.AnnotationRefName]; !exist || s == "" {
namedRef, err := reference.Parse(ref)
if err != nil {
return nil, err
}

if reference.IsNameTagged(namedRef) {
desc.Annotations[ocispec.AnnotationRefName] = namedRef.(reference.Tagged).Tag()
}
}

exportedStream, err := wrapperCli.client.Export(ctx, exporter, desc)
if err != nil {
return nil, err
}

return exportedStream, nil
}

// ImportImage creates a set of images by tarstream.
//
// NOTE: One tar may have several manifests.
Expand Down
2 changes: 2 additions & 0 deletions ctrd/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type ImageAPIClient interface {
RemoveImage(ctx context.Context, ref string) error
// ImportImage creates a set of images by tarstream.
ImportImage(ctx context.Context, importer ctrdmetaimages.Importer, reader io.Reader) ([]containerd.Image, error)
// SaveImage saves image to tarstream
SaveImage(ctx context.Context, exporter ctrdmetaimages.Exporter, ref string) (io.ReadCloser, error)
}

// SnapshotAPIClient provides access to containerd snapshot features
Expand Down
3 changes: 3 additions & 0 deletions daemon/mgr/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type ImageMgr interface {

// LoadImage creates a set of images by tarstream.
LoadImage(ctx context.Context, imageName string, tarstream io.ReadCloser) error

// SaveImage saves image to tarstream.
SaveImage(ctx context.Context, idOrRef string) (io.ReadCloser, error)
}

// ImageManager is an implementation of interface ImageMgr.
Expand Down
2 changes: 1 addition & 1 deletion daemon/mgr/image_load.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (mgr *ImageManager) LoadImage(ctx context.Context, imageName string, tarstr
}

// FIXME(fuwei): if the store fails to update reference cache, the daemon
// may fails to load after restart.
// may fail to load after restart.
merrs := new(multierror.Multierrors)
for _, img := range imgs {
if err := mgr.storeImageReference(ctx, img); err != nil {
Expand Down
23 changes: 23 additions & 0 deletions daemon/mgr/image_save.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package mgr

import (
"context"
"io"

ociimage "github.com/containerd/containerd/images/oci"
)

// SaveImage saves image to the oci.v1 format tarstream.
func (mgr *ImageManager) SaveImage(ctx context.Context, idOrRef string) (io.ReadCloser, error) {
_, _, ref, err := mgr.CheckReference(ctx, idOrRef)
if err != nil {
return nil, err
}

exportedStream, err := mgr.client.SaveImage(ctx, &ociimage.V1Exporter{}, ref.String())
if err != nil {
return nil, err
}

return exportedStream, nil
}
22 changes: 0 additions & 22 deletions test/cli_load_test.go

This file was deleted.

Loading

0 comments on commit 6f1e362

Please sign in to comment.