-
Notifications
You must be signed in to change notification settings - Fork 43
/
fixup.go
346 lines (307 loc) · 12.6 KB
/
fixup.go
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package remotes
import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/cnabio/cnab-go/bundle"
"github.com/cnabio/cnab-to-oci/relocation"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/remotes"
"github.com/docker/distribution/reference"
"github.com/hashicorp/go-multierror"
ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// FixupBundle checks that all the references are present in the referenced repository, otherwise it will mount all
// the manifests to that repository. The bundle is then patched with the new digested references.
func FixupBundle(ctx context.Context, b *bundle.Bundle, ref reference.Named, resolver remotes.Resolver, opts ...FixupOption) (relocation.ImageRelocationMap, error) {
logger := log.G(ctx)
logger.Debugf("Fixing up bundle %s", ref)
// Configure the fixup and the event loop
cfg, err := newFixupConfig(b, ref, resolver, opts...)
if err != nil {
return nil, err
}
events := make(chan FixupEvent)
eventLoopDone := make(chan struct{})
defer func() {
close(events)
// wait for all queued events to be treated
<-eventLoopDone
}()
go func() {
defer close(eventLoopDone)
for ev := range events {
cfg.eventCallback(ev)
}
}()
// Fixup invocation images
if len(b.InvocationImages) != 1 {
return nil, fmt.Errorf("only one invocation image supported for bundle %q", ref)
}
relocationMap := cfg.relocationMap
if err := fixupImage(ctx, "InvocationImage", &b.InvocationImages[0].BaseImage, relocationMap, cfg, events, cfg.invocationImagePlatformFilter); err != nil {
return nil, err
}
// Fixup images
for name, original := range b.Images {
if err := fixupImage(ctx, name, &original.BaseImage, relocationMap, cfg, events, cfg.componentImagePlatformFilter); err != nil {
return nil, err
}
b.Images[name] = original
}
logger.Debug("Bundle fixed")
return relocationMap, nil
}
func fixupImage(
ctx context.Context,
name string,
baseImage *bundle.BaseImage,
relocationMap relocation.ImageRelocationMap,
cfg fixupConfig,
events chan<- FixupEvent,
platformFilter platforms.Matcher) error {
// Fixup the base image, using the relocated base image if available
sourceImage := *baseImage
if relocatedBaseImage, ok := relocationMap[baseImage.Image]; ok {
sourceImage.Image = relocatedBaseImage
}
log.G(ctx).Debugf("Updating entry in relocation map for %q", baseImage.Image)
ctx = withMutedContext(ctx)
notifyEvent, progress := makeEventNotifier(events, sourceImage.Image, cfg.targetRef)
notifyEvent(FixupEventTypeCopyImageStart, "", nil)
fixupInfo, pushed, err := fixupBaseImage(ctx, name, &sourceImage, cfg)
if err != nil {
return notifyError(notifyEvent, err)
}
// Update the relocation map with the original image name and the digested reference of the image pushed inside the bundle repository
newRef, err := reference.WithDigest(fixupInfo.targetRepo, fixupInfo.resolvedDescriptor.Digest)
if err != nil {
return err
}
relocationMap[baseImage.Image] = newRef.String()
// if the autoUpdateBundle flag is passed, mutate the bundle with the resolved digest, mediaType, and size
if cfg.autoBundleUpdate {
baseImage.Digest = fixupInfo.resolvedDescriptor.Digest.String()
baseImage.Size = uint64(fixupInfo.resolvedDescriptor.Size)
baseImage.MediaType = fixupInfo.resolvedDescriptor.MediaType
} else {
if baseImage.Digest != fixupInfo.resolvedDescriptor.Digest.String() {
return fmt.Errorf("image %q digest differs %q after fixup: %q", baseImage.Image, baseImage.Digest, fixupInfo.resolvedDescriptor.Digest.String())
}
if baseImage.Size != uint64(fixupInfo.resolvedDescriptor.Size) {
return fmt.Errorf("image %q size differs %d after fixup: %d", baseImage.Image, baseImage.Size, fixupInfo.resolvedDescriptor.Size)
}
if baseImage.MediaType != fixupInfo.resolvedDescriptor.MediaType {
return fmt.Errorf("image %q media type differs %q after fixup: %q", baseImage.Image, baseImage.MediaType, fixupInfo.resolvedDescriptor.MediaType)
}
}
if pushed {
notifyEvent(FixupEventTypeCopyImageEnd, "Image has been pushed for service "+name, nil)
return nil
}
if fixupInfo.sourceRef.Name() == fixupInfo.targetRepo.Name() {
notifyEvent(FixupEventTypeCopyImageEnd, "Nothing to do: image reference is already present in repository"+fixupInfo.targetRepo.String(), nil)
return nil
}
sourceFetcher, err := makeSourceFetcher(ctx, cfg.resolver, fixupInfo.sourceRef.Name())
if err != nil {
return notifyError(notifyEvent, err)
}
// Fixup platforms
if err := fixupPlatforms(ctx, baseImage, relocationMap, &fixupInfo, sourceFetcher, platformFilter); err != nil {
return notifyError(notifyEvent, err)
}
// Prepare and run the copier
walkerDep, cleaner, err := makeManifestWalker(ctx, sourceFetcher, notifyEvent, cfg, fixupInfo, progress)
if err != nil {
return notifyError(notifyEvent, err)
}
defer cleaner()
if err = walkerDep.wait(); err != nil {
return notifyError(notifyEvent, err)
}
notifyEvent(FixupEventTypeCopyImageEnd, "", nil)
return nil
}
func fixupPlatforms(ctx context.Context,
baseImage *bundle.BaseImage,
relocationMap relocation.ImageRelocationMap,
fixupInfo *imageFixupInfo,
sourceFetcher sourceFetcherAdder,
filter platforms.Matcher) error {
logger := log.G(ctx)
logger.Debugf("Fixup platforms for image %v, with relocation map %v", baseImage, relocationMap)
if filter == nil ||
(fixupInfo.resolvedDescriptor.MediaType != ocischemav1.MediaTypeImageIndex &&
fixupInfo.resolvedDescriptor.MediaType != images.MediaTypeDockerSchema2ManifestList) {
// no platform filter if platform is empty, or if the descriptor is not an OCI Index / Docker Manifest list
return nil
}
reader, err := sourceFetcher.Fetch(ctx, fixupInfo.resolvedDescriptor)
if err != nil {
return err
}
defer reader.Close()
manifestBytes, err := io.ReadAll(reader)
if err != nil {
return err
}
var manifestList typelessManifestList
if err := json.Unmarshal(manifestBytes, &manifestList); err != nil {
return err
}
var validManifests []typelessDescriptor
for _, d := range manifestList.Manifests {
if d.Platform != nil && filter.Match(*d.Platform) {
validManifests = append(validManifests, d)
}
}
if len(validManifests) == 0 {
return fmt.Errorf("no descriptor matching the platform filter found in %q", fixupInfo.sourceRef)
}
manifestList.Manifests = validManifests
manifestBytes, err = json.Marshal(&manifestList)
if err != nil {
return err
}
d := sourceFetcher.Add(manifestBytes)
descriptor := fixupInfo.resolvedDescriptor
descriptor.Digest = d
descriptor.Size = int64(len(manifestBytes))
fixupInfo.resolvedDescriptor = descriptor
return nil
}
func fixupBaseImage(ctx context.Context, name string, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, error) {
// Check image references
if err := checkBaseImage(baseImage); err != nil {
return imageFixupInfo{}, false, fmt.Errorf("invalid image %q for service %q: %s", baseImage.Image, name, err)
}
targetRepoOnly, err := reference.ParseNormalizedNamed(cfg.targetRef.Name())
if err != nil {
return imageFixupInfo{}, false, err
}
fixups := []func(context.Context, reference.Named, *bundle.BaseImage, fixupConfig) (imageFixupInfo, bool, bool, error){
pushByDigest,
resolveImageInRelocationMap,
resolveImage,
pushLocalImage,
}
var bigErr *multierror.Error
for _, f := range fixups {
info, pushed, ok, err := f(ctx, targetRepoOnly, baseImage, cfg)
if err != nil {
log.G(ctx).Debug(err)
// do not stop trying fixups after the first error. Only report the errors if all fixups were unable to push the image.
bigErr = multierror.Append(bigErr, fmt.Errorf("failed to fixup the image %s for service %q: %v", baseImage.Image, name, err))
}
if ok {
return info, pushed, nil
}
}
return imageFixupInfo{}, false, bigErr.ErrorOrNil()
}
func pushByDigest(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) {
if baseImage.Image != "" || !cfg.pushImages {
return imageFixupInfo{}, false, false, nil
}
descriptor, err := pushImageToTarget(ctx, baseImage.Digest, cfg)
if err != nil {
return imageFixupInfo{}, false, false, fmt.Errorf("failed to push digested image %s@%s to target %s: %v", baseImage.Image, baseImage.Digest, target, err)
}
return imageFixupInfo{
targetRepo: target,
sourceRef: nil,
resolvedDescriptor: descriptor,
}, true, true, nil
}
func resolveImage(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) {
sourceImageRef, err := ref(baseImage.Image)
if err != nil {
return imageFixupInfo{}, false, false, fmt.Errorf("failed to resolve image: invalid source ref %s: %w", baseImage.Image, err)
}
_, descriptor, err := cfg.resolver.Resolve(ctx, sourceImageRef.String())
if err != nil {
return imageFixupInfo{}, false, false, fmt.Errorf("failed to resolve image %s: %w", sourceImageRef.String(), err)
}
return imageFixupInfo{
targetRepo: target,
sourceRef: sourceImageRef,
resolvedDescriptor: descriptor,
}, false, true, nil
}
func resolveImageInRelocationMap(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) {
sourceImageRef, err := ref(baseImage.Image)
if err != nil {
return imageFixupInfo{}, false, false, fmt.Errorf("failed to resolve image in relocation map: invalid source ref %s: %v", baseImage.Image, err)
}
relocatedRef, ok := cfg.relocationMap[baseImage.Image]
if !ok {
return imageFixupInfo{}, false, false, nil
}
relocatedImageRef, err := ref(relocatedRef)
if err != nil {
return imageFixupInfo{}, false, false, fmt.Errorf("failed to resolve image in relocation map: invalid target ref %s: %v", relocatedRef, err)
}
_, descriptor, err := cfg.resolver.Resolve(ctx, relocatedImageRef.String())
if err != nil {
return imageFixupInfo{}, false, false, err
}
return imageFixupInfo{
targetRepo: target,
sourceRef: sourceImageRef,
resolvedDescriptor: descriptor,
}, false, true, nil
}
func pushLocalImage(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) {
if !cfg.pushImages {
return imageFixupInfo{}, false, false, nil
}
sourceImageRef, err := ref(baseImage.Image)
if err != nil {
return imageFixupInfo{}, false, false, fmt.Errorf("failed to push local image: invalid source ref %s: %v", baseImage.Image, err)
}
descriptor, err := pushImageToTarget(ctx, baseImage.Image, cfg)
if err != nil {
return imageFixupInfo{}, false, false, fmt.Errorf("failed to push local image %s: %v", baseImage.Image, err)
}
return imageFixupInfo{
targetRepo: target,
sourceRef: sourceImageRef,
resolvedDescriptor: descriptor,
}, true, true, nil
}
func ref(str string) (reference.Named, error) {
r, err := reference.ParseNormalizedNamed(str)
if err != nil {
return nil, fmt.Errorf("%q is not a valid reference: %v", str, err)
}
return reference.TagNameOnly(r), nil
}
// pushImageToTarget pushes the image from the local docker daemon store to the target defined in the configuration.
// Docker image cannot be pushed by digest to a registry. So to be able to push the image inside the targeted repository
// the same behaviour than for multi architecture images is used: all the images are tagged for the targeted repository
// and then pushed.
// Every time a new image is pushed under a tag, the previous tagged image will be untagged. But this untagged image
// remains accessible using its digest. So right after pushing it, the image is resolved to grab its digest from the
// registry and can be added to the index.
// The final workflow is then:
// - tag the image to push with targeted reference
// - push the image using a docker `ImageAPIClient`
// - resolve the pushed image to grab its digest
func pushImageToTarget(ctx context.Context, src string, cfg fixupConfig) (ocischemav1.Descriptor, error) {
taggedRef := reference.TagNameOnly(cfg.targetRef)
if err := cfg.imageClient.ImageTag(ctx, src, cfg.targetRef.String()); err != nil {
return ocischemav1.Descriptor{}, fmt.Errorf("failed to push image %q, make sure the image exists locally: %s", src, err)
}
if err := pushTaggedImage(ctx, cfg.imageClient, cfg.targetRef, cfg.pushOut); err != nil {
return ocischemav1.Descriptor{}, fmt.Errorf("failed to push image %q: %s", src, err)
}
_, descriptor, err := cfg.resolver.Resolve(ctx, taggedRef.String())
if err != nil {
return ocischemav1.Descriptor{}, fmt.Errorf("failed to resolve %q after pushing it: %s", taggedRef, err)
}
return descriptor, nil
}