summaryrefslogtreecommitdiff
path: root/plugin/fetch_linux.go
blob: bfad62c315020894cc65ff3117312346dc59a253 (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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package plugin

import (
	"context"
	"io"
	"net/http"
	"time"

	"github.com/containerd/containerd/content"
	cerrdefs "github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/images"
	"github.com/containerd/containerd/remotes"
	"github.com/containerd/containerd/remotes/docker"
	"github.com/docker/distribution/reference"
	"github.com/docker/docker/api/types/registry"
	progressutils "github.com/docker/docker/distribution/utils"
	"github.com/docker/docker/pkg/chrootarchive"
	"github.com/docker/docker/pkg/ioutils"
	"github.com/docker/docker/pkg/progress"
	"github.com/docker/docker/pkg/stringid"
	"github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

const mediaTypePluginConfig = "application/vnd.docker.plugin.v1+json"

// setupProgressOutput sets up the passed in writer to stream progress.
//
// The passed in cancel function is used by the progress writer to signal callers that there
// is an issue writing to the stream.
//
// The returned function is used to wait for the progress writer to be finished.
// Call it to make sure the progress writer is done before returning from your function as needed.
func setupProgressOutput(outStream io.Writer, cancel func()) (progress.Output, func()) {
	var out progress.Output
	f := func() {}

	if outStream != nil {
		ch := make(chan progress.Progress, 100)
		out = progress.ChanOutput(ch)

		ctx, retCancel := context.WithCancel(context.Background())
		go func() {
			progressutils.WriteDistributionProgress(cancel, outStream, ch)
			retCancel()
		}()

		f = func() {
			close(ch)
			<-ctx.Done()
		}
	} else {
		out = progress.DiscardOutput()
	}
	return out, f
}

// fetch the content related to the passed in reference into the blob store and appends the provided images.Handlers
// There is no need to use remotes.FetchHandler since it already gets set
func (pm *Manager) fetch(ctx context.Context, ref reference.Named, auth *registry.AuthConfig, out progress.Output, metaHeader http.Header, handlers ...images.Handler) (err error) {
	// We need to make sure we have a domain on the reference
	withDomain, err := reference.ParseNormalizedNamed(ref.String())
	if err != nil {
		return errors.Wrap(err, "error parsing plugin image reference")
	}

	// Make sure we can authenticate the request since the auth scope for plugin repos is different than a normal repo.
	ctx = docker.WithScope(ctx, scope(ref, false))

	// Make sure the fetch handler knows how to set a ref key for the plugin media type.
	// Without this the ref key is "unknown" and we see a nasty warning message in the logs
	ctx = remotes.WithMediaTypeKeyPrefix(ctx, mediaTypePluginConfig, "docker-plugin")

	resolver, err := pm.newResolver(ctx, nil, auth, metaHeader, false)
	if err != nil {
		return err
	}
	resolved, desc, err := resolver.Resolve(ctx, withDomain.String())
	if err != nil {
		// This is backwards compatible with older versions of the distribution registry.
		// The containerd client will add it's own accept header as a comma separated list of supported manifests.
		// This is perfectly fine, unless you are talking to an older registry which does not split the comma separated list,
		//   so it is never able to match a media type and it falls back to schema1 (yuck) and fails because our manifest the
		//   fallback does not support plugin configs...
		logrus.WithError(err).WithField("ref", withDomain).Debug("Error while resolving reference, falling back to backwards compatible accept header format")
		headers := http.Header{}
		headers.Add("Accept", images.MediaTypeDockerSchema2Manifest)
		headers.Add("Accept", images.MediaTypeDockerSchema2ManifestList)
		headers.Add("Accept", ocispec.MediaTypeImageManifest)
		headers.Add("Accept", ocispec.MediaTypeImageIndex)
		resolver, _ = pm.newResolver(ctx, nil, auth, headers, false)
		if resolver != nil {
			resolved, desc, err = resolver.Resolve(ctx, withDomain.String())
			if err != nil {
				logrus.WithError(err).WithField("ref", withDomain).Debug("Failed to resolve reference after falling back to backwards compatible accept header format")
			}
		}
		if err != nil {
			return errors.Wrap(err, "error resolving plugin reference")
		}
	}

	fetcher, err := resolver.Fetcher(ctx, resolved)
	if err != nil {
		return errors.Wrap(err, "error creating plugin image fetcher")
	}

	fp := withFetchProgress(pm.blobStore, out, ref)
	handlers = append([]images.Handler{fp, remotes.FetchHandler(pm.blobStore, fetcher)}, handlers...)
	return images.Dispatch(ctx, images.Handlers(handlers...), nil, desc)
}

// applyLayer makes an images.HandlerFunc which applies a fetched image rootfs layer to a directory.
//
// TODO(@cpuguy83) This gets run sequentially after layer pull (makes sense), however
// if there are multiple layers to fetch we may end up extracting layers in the wrong
// order.
func applyLayer(cs content.Store, dir string, out progress.Output) images.HandlerFunc {
	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
		switch desc.MediaType {
		case
			ocispec.MediaTypeImageLayer,
			images.MediaTypeDockerSchema2Layer,
			ocispec.MediaTypeImageLayerGzip,
			images.MediaTypeDockerSchema2LayerGzip:
		default:
			return nil, nil
		}

		ra, err := cs.ReaderAt(ctx, desc)
		if err != nil {
			return nil, errors.Wrapf(err, "error getting content from content store for digest %s", desc.Digest)
		}

		id := stringid.TruncateID(desc.Digest.String())

		rc := ioutils.NewReadCloserWrapper(content.NewReader(ra), ra.Close)
		pr := progress.NewProgressReader(rc, out, desc.Size, id, "Extracting")
		defer pr.Close()

		if _, err := chrootarchive.ApplyLayer(dir, pr); err != nil {
			return nil, errors.Wrapf(err, "error applying layer for digest %s", desc.Digest)
		}
		progress.Update(out, id, "Complete")
		return nil, nil
	}
}

func childrenHandler(cs content.Store) images.HandlerFunc {
	ch := images.ChildrenHandler(cs)
	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
		switch desc.MediaType {
		case mediaTypePluginConfig:
			return nil, nil
		default:
			return ch(ctx, desc)
		}
	}
}

type fetchMeta struct {
	blobs    []digest.Digest
	config   digest.Digest
	manifest digest.Digest
}

func storeFetchMetadata(m *fetchMeta) images.HandlerFunc {
	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
		switch desc.MediaType {
		case
			images.MediaTypeDockerSchema2LayerForeignGzip,
			images.MediaTypeDockerSchema2Layer,
			ocispec.MediaTypeImageLayer,
			ocispec.MediaTypeImageLayerGzip:
			m.blobs = append(m.blobs, desc.Digest)
		case ocispec.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest:
			m.manifest = desc.Digest
		case mediaTypePluginConfig:
			m.config = desc.Digest
		}
		return nil, nil
	}
}

func validateFetchedMetadata(md fetchMeta) error {
	if md.config == "" {
		return errors.New("fetched plugin image but plugin config is missing")
	}
	if md.manifest == "" {
		return errors.New("fetched plugin image but manifest is missing")
	}
	return nil
}

// withFetchProgress is a fetch handler which registers a descriptor with a progress
func withFetchProgress(cs content.Store, out progress.Output, ref reference.Named) images.HandlerFunc {
	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
		switch desc.MediaType {
		case ocispec.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest:
			tn := reference.TagNameOnly(ref)
			tagged := tn.(reference.Tagged)
			progress.Messagef(out, tagged.Tag(), "Pulling from %s", reference.FamiliarName(ref))
			progress.Messagef(out, "", "Digest: %s", desc.Digest.String())
			return nil, nil
		case
			images.MediaTypeDockerSchema2LayerGzip,
			images.MediaTypeDockerSchema2Layer,
			ocispec.MediaTypeImageLayer,
			ocispec.MediaTypeImageLayerGzip:
		default:
			return nil, nil
		}

		id := stringid.TruncateID(desc.Digest.String())

		if _, err := cs.Info(ctx, desc.Digest); err == nil {
			out.WriteProgress(progress.Progress{ID: id, Action: "Already exists", LastUpdate: true})
			return nil, nil
		}

		progress.Update(out, id, "Waiting")

		key := remotes.MakeRefKey(ctx, desc)

		go func() {
			timer := time.NewTimer(100 * time.Millisecond)
			if !timer.Stop() {
				<-timer.C
			}
			defer timer.Stop()

			var pulling bool
			var ctxErr error

			for {
				timer.Reset(100 * time.Millisecond)

				select {
				case <-ctx.Done():
					ctxErr = ctx.Err()
					// make sure we can still fetch from the content store
					// TODO: Might need to add some sort of timeout
					ctx = context.Background()
				case <-timer.C:
				}

				s, err := cs.Status(ctx, key)
				if err != nil {
					if !cerrdefs.IsNotFound(err) {
						logrus.WithError(err).WithField("layerDigest", desc.Digest.String()).Error("Error looking up status of plugin layer pull")
						progress.Update(out, id, err.Error())
						return
					}

					if _, err := cs.Info(ctx, desc.Digest); err == nil {
						progress.Update(out, id, "Download complete")
						return
					}

					if ctxErr != nil {
						progress.Update(out, id, ctxErr.Error())
						return
					}

					continue
				}

				if !pulling {
					progress.Update(out, id, "Pulling fs layer")
					pulling = true
				}

				if s.Offset == s.Total {
					out.WriteProgress(progress.Progress{ID: id, Action: "Download complete", Current: s.Offset, LastUpdate: true})
					return
				}

				out.WriteProgress(progress.Progress{ID: id, Action: "Downloading", Current: s.Offset, Total: s.Total})
			}
		}()
		return nil, nil
	}
}