Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/server/router/image/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ type imageBackend interface {
}

type importExportBackend interface {
LoadImage(inTar io.ReadCloser, outStream io.Writer, quiet bool) error
LoadImage(inTar io.ReadCloser, outStream io.Writer, name string, refs map[string]string, quiet bool) error
ImportImage(src string, repository, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error
ExportImage(names []string, outStream io.Writer) error
ExportImage(names []string, format string, refs map[string]string, outStream io.Writer) error
}

type registryBackend interface {
Expand Down
21 changes: 19 additions & 2 deletions api/server/router/image/image_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,16 @@ func (s *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter, r
names = r.Form["names"]
}

if err := s.backend.ExportImage(names, output); err != nil {
format := r.FormValue("format")
refs := map[string]string{}
refsJSON := r.FormValue("refs")
if refsJSON != "" {
if err := json.NewDecoder(strings.NewReader(refsJSON)).Decode(&refs); err != nil {
return err
}
}

if err := s.backend.ExportImage(names, format, refs, output); err != nil {
if !output.Flushed() {
return err
}
Expand All @@ -201,12 +210,20 @@ func (s *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter,
return err
}
quiet := httputils.BoolValueOrDefault(r, "quiet", true)
name := r.FormValue("name")
var refs = map[string]string{}
refsJSON := r.FormValue("refs")
if refsJSON != "" {
if err := json.NewDecoder(strings.NewReader(refsJSON)).Decode(&refs); err != nil {
return err
}
}

w.Header().Set("Content-Type", "application/json")

output := ioutils.NewWriteFlusher(w)
defer output.Close()
if err := s.backend.LoadImage(r.Body, output, quiet); err != nil {
if err := s.backend.LoadImage(r.Body, output, name, refs, quiet); err != nil {
output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
}
return nil
Expand Down
15 changes: 15 additions & 0 deletions api/types/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,21 @@ func (h *HijackedResponse) CloseWrite() error {
return nil
}

// ImageSaveOptions holds the information
// necessary to save a set of images.
type ImageSaveOptions struct {
Format string
Refs map[string]string
}

// ImageLoadOptions holds the information
// necessary to load a set of images.
type ImageLoadOptions struct {
Quiet bool
Name string
Refs map[string]string
}

// ImageBuildOptions holds the information
// necessary to build images.
type ImageBuildOptions struct {
Expand Down
15 changes: 13 additions & 2 deletions cli/command/image/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ import (

"golang.org/x/net/context"

"github.com/docker/docker/api/types"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/system"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/spf13/cobra"
)

type loadOptions struct {
input string
quiet bool
oci bool
name string
refs []string
}

// NewLoadCommand creates a new `docker load` command
Expand All @@ -35,12 +40,13 @@ func NewLoadCommand(dockerCli *command.DockerCli) *cobra.Command {

flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output")
flags.StringVarP(&opts.name, "name", "n", "", "Name to use when loading OCI image layout tar archive")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best to not have the -n support, only --name
Also not particularly clear what this does.
I assume the oci image layout can have multiple images?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the oci image layout can have multiple images?

Yes, it does. Please refer to @stevvooe's proposal here #25779

flags.StringSliceVar(&opts.refs, "ref", []string{}, "References to use when loading an OCI image layout tar archive")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the loaded image is going to be tagged with this reference...
This would be inconsistent with build, which uses -t and --tag... which of course also only accepts a single value currently.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe I'm mixing name and ref? Either case it would be good to make sure we don't add more inconsistency here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OCI is a bit different - a ref flag here translates an OCI reference (essentially a tag) e.g. latest to a docker named reference.
The thing is OCI doesn't have naming, so what we get is just a tarball containing multiple references (potentially, even different images).

Not sure how to address this comment about docker build -t - it's something a bit different to me.


return cmd
}

func runLoad(dockerCli *command.DockerCli, opts loadOptions) error {

var input io.Reader = dockerCli.In()
if opts.input != "" {
// We use system.OpenSequential to use sequential file access on Windows, avoiding
Expand All @@ -62,7 +68,12 @@ func runLoad(dockerCli *command.DockerCli, opts loadOptions) error {
if !dockerCli.Out().IsTerminal() {
opts.quiet = true
}
response, err := dockerCli.Client().ImageLoad(context.Background(), input, opts.quiet)
imageLoadOpts := types.ImageLoadOptions{
Quiet: opts.quiet,
Name: opts.name,
Refs: runconfigopts.ConvertKVStringsToMap(opts.refs),
}
response, err := dockerCli.Client().ImageLoad(context.Background(), input, imageLoadOpts)
if err != nil {
return err
}
Expand Down
13 changes: 12 additions & 1 deletion cli/command/image/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import (

"golang.org/x/net/context"

"github.com/docker/docker/api/types"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/spf13/cobra"
)

type saveOptions struct {
images []string
output string
format string
refs []string
}

// NewSaveCommand creates a new `docker save` command
Expand All @@ -33,6 +37,8 @@ func NewSaveCommand(dockerCli *command.DockerCli) *cobra.Command {
flags := cmd.Flags()

flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
flags.StringVarP(&opts.format, "format", "f", "", "Specify the format of the output tar archive")
flags.StringSliceVar(&opts.refs, "ref", []string{}, "References to use when loading an OCI image layout tar archive")

return cmd
}
Expand All @@ -42,7 +48,12 @@ func runSave(dockerCli *command.DockerCli, opts saveOptions) error {
return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.")
}

responseBody, err := dockerCli.Client().ImageSave(context.Background(), opts.images)
imageSaveOpts := types.ImageSaveOptions{
Format: opts.format,
Refs: runconfigopts.ConvertKVStringsToMap(opts.refs),
}

responseBody, err := dockerCli.Client().ImageSave(context.Background(), opts.images, imageSaveOpts)
if err != nil {
return err
}
Expand Down
11 changes: 9 additions & 2 deletions client/image_load.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"encoding/json"
"io"
"net/url"

Expand All @@ -12,12 +13,18 @@ import (
// ImageLoad loads an image in the docker host from the client host.
// It's up to the caller to close the io.ReadCloser in the
// ImageLoadResponse returned by this function.
func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, opts types.ImageLoadOptions) (types.ImageLoadResponse, error) {
v := url.Values{}
v.Set("quiet", "0")
if quiet {
if opts.Quiet {
v.Set("quiet", "1")
}
refsJSON, err := json.Marshal(opts.Refs)
if err != nil {
return types.ImageLoadResponse{}, err
}
v.Set("refs", string(refsJSON))
v.Set("name", opts.Name)
headers := map[string][]string{"Content-Type": {"application/x-tar"}}
resp, err := cli.postRaw(ctx, "/images/load", v, input, headers)
if err != nil {
Expand Down
6 changes: 4 additions & 2 deletions client/image_load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strings"
"testing"

"github.com/docker/docker/api/types"

"golang.org/x/net/context"
)

Expand All @@ -16,7 +18,7 @@ func TestImageLoadError(t *testing.T) {
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}

_, err := client.ImageLoad(context.Background(), nil, true)
_, err := client.ImageLoad(context.Background(), nil, types.ImageLoadOptions{Quiet: true})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
Expand Down Expand Up @@ -77,7 +79,7 @@ func TestImageLoad(t *testing.T) {
}

input := bytes.NewReader([]byte(expectedInput))
imageLoadResponse, err := client.ImageLoad(context.Background(), input, loadCase.quiet)
imageLoadResponse, err := client.ImageLoad(context.Background(), input, types.ImageLoadOptions{Quiet: loadCase.quiet})
if err != nil {
t.Fatal(err)
}
Expand Down
13 changes: 11 additions & 2 deletions client/image_save.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
package client

import (
"encoding/json"
"io"
"net/url"

"github.com/docker/docker/api/types"

"golang.org/x/net/context"
)

// ImageSave retrieves one or more images from the docker host as an io.ReadCloser.
// It's up to the caller to store the images and close the stream.
func (cli *Client) ImageSave(ctx context.Context, imageIDs []string) (io.ReadCloser, error) {
func (cli *Client) ImageSave(ctx context.Context, images []string, opts types.ImageSaveOptions) (io.ReadCloser, error) {
query := url.Values{
"names": imageIDs,
"names": images,
}
query.Set("format", opts.Format)
refsJSON, err := json.Marshal(opts.Refs)
if err != nil {
return nil, err
}
query.Set("refs", string(refsJSON))

resp, err := cli.get(ctx, "/images/get", query, nil)
if err != nil {
Expand Down
6 changes: 4 additions & 2 deletions client/image_save_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"reflect"
"testing"

"github.com/docker/docker/api/types"

"golang.org/x/net/context"

"strings"
Expand All @@ -17,7 +19,7 @@ func TestImageSaveError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageSave(context.Background(), []string{"nothing"})
_, err := client.ImageSave(context.Background(), []string{"nothing"}, types.ImageSaveOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
Expand All @@ -43,7 +45,7 @@ func TestImageSave(t *testing.T) {
}, nil
}),
}
saveResponse, err := client.ImageSave(context.Background(), []string{"image_id1", "image_id2"})
saveResponse, err := client.ImageSave(context.Background(), []string{"image_id1", "image_id2"}, types.ImageSaveOptions{})
if err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 2 additions & 2 deletions client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ type ImageAPIClient interface {
ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error)
ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error)
ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error)
ImageLoad(ctx context.Context, input io.Reader, options types.ImageLoadOptions) (types.ImageLoadResponse, error)
ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error)
ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error)
ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error)
ImageSave(ctx context.Context, images []string) (io.ReadCloser, error)
ImageSave(ctx context.Context, images []string, options types.ImageSaveOptions) (io.ReadCloser, error)
ImageTag(ctx context.Context, image, ref string) error
ImagesPrune(ctx context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error)
}
Expand Down
27 changes: 20 additions & 7 deletions daemon/image_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,30 @@ import (
// ExportImage exports a list of images to the given output stream. The
// exported images are archived into a tar when written to the output
// stream. All images with the given tag and all versions containing
// the same tag are exported. names is the set of tags to export, and
// outStream is the writer which the images are written to.
func (daemon *Daemon) ExportImage(names []string, outStream io.Writer) error {
imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.referenceStore, daemon)
// the same tag are exported. names is the set of tags to export.
// outStream is the writer which the images are written to. refs is a map used
// when exporting to the OCI format.
// format is the format of the resulting tar ball.
func (daemon *Daemon) ExportImage(names []string, format string, refs map[string]string, outStream io.Writer) error {
opts := &tarexport.Options{
Format: format,
Refs: refs,
Experimental: daemon.HasExperimental(),
}
imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.referenceStore, daemon, opts)
return imageExporter.Save(names, outStream)
}

// LoadImage uploads a set of images into the repository. This is the
// LoadImage loads a set of images into the repository. This is the
// complement of ImageExport. The input stream is an uncompressed tar
// ball containing images and metadata.
func (daemon *Daemon) LoadImage(inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.referenceStore, daemon)
func (daemon *Daemon) LoadImage(inTar io.ReadCloser, outStream io.Writer, name string, refs map[string]string, quiet bool) error {
opts := &tarexport.Options{
Name: name,
Refs: refs,
Experimental: daemon.HasExperimental(),
}
imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.referenceStore, daemon, opts)
// the first arg will be "name" passed down from LoadImage() itself
return imageExporter.Load(inTar, outStream, quiet)
}
Loading