summaryrefslogtreecommitdiff
path: root/vendor/github.com/containers/common/libimage/save.go
blob: a42bbb4973ebfd39e16d9434ad730af45529cc23 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
package libimage

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	dirTransport "github.com/containers/image/v5/directory"
	dockerArchiveTransport "github.com/containers/image/v5/docker/archive"
	"github.com/containers/image/v5/docker/reference"
	"github.com/containers/image/v5/manifest"
	ociArchiveTransport "github.com/containers/image/v5/oci/archive"
	ociTransport "github.com/containers/image/v5/oci/layout"
	"github.com/containers/image/v5/types"
	ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/sirupsen/logrus"
)

// SaveOptions allow for customizing saving images.
type SaveOptions struct {
	CopyOptions

	// AdditionalTags for the saved image.  Incompatible when saving
	// multiple images.
	AdditionalTags []string
}

// Save saves one or more images indicated by `names` in the specified `format`
// to `path`.  Supported formats are oci-archive, docker-archive, oci-dir and
// docker-dir.  The latter two adhere to the dir transport in the corresponding
// oci or docker v2s2 format.  Please note that only docker-archive supports
// saving more than one images.  Other formats will yield an error attempting
// to save more than one.
func (r *Runtime) Save(ctx context.Context, names []string, format, path string, options *SaveOptions) error {
	logrus.Debugf("Saving one more images (%s) to %q", names, path)

	if options == nil {
		options = &SaveOptions{}
	}

	// First some sanity checks to simplify subsequent code.
	switch len(names) {
	case 0:
		return errors.New("no image specified for saving images")
	case 1:
		// All formats support saving 1.
	default:
		if format != "docker-archive" {
			return fmt.Errorf("unsupported format %q for saving multiple images (only docker-archive)", format)
		}
		if len(options.AdditionalTags) > 0 {
			return fmt.Errorf("cannot save multiple images with multiple tags")
		}
	}

	// Dispatch the save operations.
	switch format {
	case "oci-archive", "oci-dir", "docker-dir":
		if len(names) > 1 {
			return fmt.Errorf("%q does not support saving multiple images (%v)", format, names)
		}
		return r.saveSingleImage(ctx, names[0], format, path, options)

	case "docker-archive":
		options.ManifestMIMEType = manifest.DockerV2Schema2MediaType
		return r.saveDockerArchive(ctx, names, path, options)
	}

	return fmt.Errorf("unsupported format %q for saving images", format)
}

// saveSingleImage saves the specified image name to the specified path.
// Supported formats are "oci-archive", "oci-dir" and "docker-dir".
func (r *Runtime) saveSingleImage(ctx context.Context, name, format, path string, options *SaveOptions) error {
	image, imageName, err := r.LookupImage(name, nil)
	if err != nil {
		return err
	}

	if r.eventChannel != nil {
		defer r.writeEvent(&Event{ID: image.ID(), Name: path, Time: time.Now(), Type: EventTypeImageSave})
	}

	// Unless the image was referenced by ID, use the resolved name as a
	// tag.
	var tag string
	if !strings.HasPrefix(image.ID(), imageName) {
		tag = imageName
	}

	srcRef, err := image.StorageReference()
	if err != nil {
		return err
	}

	// Prepare the destination reference.
	var destRef types.ImageReference
	switch format {
	case "oci-archive":
		destRef, err = ociArchiveTransport.NewReference(path, tag)

	case "oci-dir":
		destRef, err = ociTransport.NewReference(path, tag)
		options.ManifestMIMEType = ociv1.MediaTypeImageManifest

	case "docker-dir":
		destRef, err = dirTransport.NewReference(path)
		options.ManifestMIMEType = manifest.DockerV2Schema2MediaType

	default:
		return fmt.Errorf("unsupported format %q for saving images", format)
	}

	if err != nil {
		return err
	}

	c, err := r.newCopier(&options.CopyOptions)
	if err != nil {
		return err
	}
	defer c.close()

	_, err = c.copy(ctx, srcRef, destRef)
	return err
}

// saveDockerArchive saves the specified images indicated by names to the path.
// It loads all images from the local containers storage and assembles the meta
// data needed to properly save images.  Since multiple names could refer to
// the *same* image, we need to dance a bit and store additional "names".
// Those can then be used as additional tags when copying.
func (r *Runtime) saveDockerArchive(ctx context.Context, names []string, path string, options *SaveOptions) error {
	type localImage struct {
		image *Image
		tags  []reference.NamedTagged
	}

	additionalTags := []reference.NamedTagged{}
	for _, tag := range options.AdditionalTags {
		named, err := NormalizeName(tag)
		if err == nil {
			tagged, withTag := named.(reference.NamedTagged)
			if !withTag {
				return fmt.Errorf("invalid additional tag %q: normalized to untagged %q", tag, named.String())
			}
			additionalTags = append(additionalTags, tagged)
		}
	}

	orderedIDs := []string{}                    // to preserve the relative order
	localImages := make(map[string]*localImage) // to assemble tags
	visitedNames := make(map[string]bool)       // filters duplicate names
	for _, name := range names {
		// Look up local images.
		image, imageName, err := r.LookupImage(name, nil)
		if err != nil {
			return err
		}
		// Make sure to filter duplicates purely based on the resolved
		// name.
		if _, exists := visitedNames[imageName]; exists {
			continue
		}
		visitedNames[imageName] = true
		// Extract and assemble the data.
		local, exists := localImages[image.ID()]
		if !exists {
			local = &localImage{image: image}
			local.tags = additionalTags
			orderedIDs = append(orderedIDs, image.ID())
		}
		// Add the tag if the locally resolved name is properly tagged
		// (which it should unless we looked it up by ID).
		named, err := reference.ParseNamed(imageName)
		if err == nil {
			tagged, withTag := named.(reference.NamedTagged)
			if withTag {
				local.tags = append(local.tags, tagged)
			}
		}
		localImages[image.ID()] = local
		if r.eventChannel != nil {
			defer r.writeEvent(&Event{ID: image.ID(), Name: path, Time: time.Now(), Type: EventTypeImageSave})
		}
	}

	writer, err := dockerArchiveTransport.NewWriter(r.systemContextCopy(), path)
	if err != nil {
		return err
	}
	defer writer.Close()

	for _, id := range orderedIDs {
		local, exists := localImages[id]
		if !exists {
			return fmt.Errorf("internal error: saveDockerArchive: ID %s not found in local map", id)
		}

		copyOpts := options.CopyOptions
		copyOpts.dockerArchiveAdditionalTags = local.tags

		c, err := r.newCopier(&copyOpts)
		if err != nil {
			return err
		}
		defer c.close()

		destRef, err := writer.NewReference(nil)
		if err != nil {
			return err
		}

		srcRef, err := local.image.StorageReference()
		if err != nil {
			return err
		}

		if _, err := c.copy(ctx, srcRef, destRef); err != nil {
			return err
		}
	}

	return nil
}