1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package mutate
16
17 import (
18 "archive/tar"
19 "bytes"
20 "encoding/json"
21 "errors"
22 "fmt"
23 "io"
24 "path/filepath"
25 "strings"
26 "time"
27
28 "github.com/google/go-containerregistry/internal/gzip"
29 v1 "github.com/google/go-containerregistry/pkg/v1"
30 "github.com/google/go-containerregistry/pkg/v1/empty"
31 "github.com/google/go-containerregistry/pkg/v1/match"
32 "github.com/google/go-containerregistry/pkg/v1/partial"
33 "github.com/google/go-containerregistry/pkg/v1/tarball"
34 "github.com/google/go-containerregistry/pkg/v1/types"
35 )
36
37 const whiteoutPrefix = ".wh."
38
39
40
41 type Addendum struct {
42 Layer v1.Layer
43 History v1.History
44 URLs []string
45 Annotations map[string]string
46 MediaType types.MediaType
47 }
48
49
50 func AppendLayers(base v1.Image, layers ...v1.Layer) (v1.Image, error) {
51 additions := make([]Addendum, 0, len(layers))
52 for _, layer := range layers {
53 additions = append(additions, Addendum{Layer: layer})
54 }
55
56 return Append(base, additions...)
57 }
58
59
60 func Append(base v1.Image, adds ...Addendum) (v1.Image, error) {
61 if len(adds) == 0 {
62 return base, nil
63 }
64 if err := validate(adds); err != nil {
65 return nil, err
66 }
67
68 return &image{
69 base: base,
70 adds: adds,
71 }, nil
72 }
73
74
75
76
77 type Appendable interface {
78 MediaType() (types.MediaType, error)
79 Digest() (v1.Hash, error)
80 Size() (int64, error)
81 }
82
83
84
85 type IndexAddendum struct {
86 Add Appendable
87 v1.Descriptor
88 }
89
90
91 func AppendManifests(base v1.ImageIndex, adds ...IndexAddendum) v1.ImageIndex {
92 return &index{
93 base: base,
94 adds: adds,
95 }
96 }
97
98
99 func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) v1.ImageIndex {
100 return &index{
101 base: base,
102 remove: matcher,
103 }
104 }
105
106
107 func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
108 cf, err := base.ConfigFile()
109 if err != nil {
110 return nil, err
111 }
112
113 cf.Config = cfg
114
115 return ConfigFile(base, cf)
116 }
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131 func Subject(f partial.WithRawManifest, subject v1.Descriptor) partial.WithRawManifest {
132 if img, ok := f.(v1.Image); ok {
133 return &image{
134 base: img,
135 subject: &subject,
136 }
137 }
138 if idx, ok := f.(v1.ImageIndex); ok {
139 return &index{
140 base: idx,
141 subject: &subject,
142 }
143 }
144 return arbitraryRawManifest{a: f, subject: &subject}
145 }
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164 func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest {
165 if img, ok := f.(v1.Image); ok {
166 return &image{
167 base: img,
168 annotations: anns,
169 }
170 }
171 if idx, ok := f.(v1.ImageIndex); ok {
172 return &index{
173 base: idx,
174 annotations: anns,
175 }
176 }
177 return arbitraryRawManifest{a: f, anns: anns}
178 }
179
180 type arbitraryRawManifest struct {
181 a partial.WithRawManifest
182 anns map[string]string
183 subject *v1.Descriptor
184 }
185
186 func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
187 b, err := a.a.RawManifest()
188 if err != nil {
189 return nil, err
190 }
191 var m map[string]any
192 if err := json.Unmarshal(b, &m); err != nil {
193 return nil, err
194 }
195 if ann, ok := m["annotations"]; ok {
196 if annm, ok := ann.(map[string]string); ok {
197 for k, v := range a.anns {
198 annm[k] = v
199 }
200 } else {
201 return nil, fmt.Errorf(".annotations is not a map: %T", ann)
202 }
203 } else {
204 m["annotations"] = a.anns
205 }
206 if a.subject != nil {
207 m["subject"] = a.subject
208 }
209 return json.Marshal(m)
210 }
211
212
213 func ConfigFile(base v1.Image, cfg *v1.ConfigFile) (v1.Image, error) {
214 m, err := base.Manifest()
215 if err != nil {
216 return nil, err
217 }
218
219 image := &image{
220 base: base,
221 manifest: m.DeepCopy(),
222 configFile: cfg,
223 }
224
225 return image, nil
226 }
227
228
229 func CreatedAt(base v1.Image, created v1.Time) (v1.Image, error) {
230 cf, err := base.ConfigFile()
231 if err != nil {
232 return nil, err
233 }
234
235 cfg := cf.DeepCopy()
236 cfg.Created = created
237
238 return ConfigFile(base, cfg)
239 }
240
241
242
243
244
245
246
247
248
249 func Extract(img v1.Image) io.ReadCloser {
250 pr, pw := io.Pipe()
251
252 go func() {
253
254
255
256
257 pw.CloseWithError(extract(img, pw))
258 }()
259
260 return pr
261 }
262
263
264 func extract(img v1.Image, w io.Writer) error {
265 tarWriter := tar.NewWriter(w)
266 defer tarWriter.Close()
267
268 fileMap := map[string]bool{}
269
270 layers, err := img.Layers()
271 if err != nil {
272 return fmt.Errorf("retrieving image layers: %w", err)
273 }
274
275
276
277
278 for i := len(layers) - 1; i >= 0; i-- {
279 layer := layers[i]
280 layerReader, err := layer.Uncompressed()
281 if err != nil {
282 return fmt.Errorf("reading layer contents: %w", err)
283 }
284 defer layerReader.Close()
285 tarReader := tar.NewReader(layerReader)
286 for {
287 header, err := tarReader.Next()
288 if errors.Is(err, io.EOF) {
289 break
290 }
291 if err != nil {
292 return fmt.Errorf("reading tar: %w", err)
293 }
294
295
296
297 header.Name = filepath.Clean(header.Name)
298
299
300
301 header.Format = tar.FormatPAX
302
303 basename := filepath.Base(header.Name)
304 dirname := filepath.Dir(header.Name)
305 tombstone := strings.HasPrefix(basename, whiteoutPrefix)
306 if tombstone {
307 basename = basename[len(whiteoutPrefix):]
308 }
309
310
311
312 var name string
313 if header.Typeflag == tar.TypeDir {
314 name = header.Name
315 } else {
316 name = filepath.Join(dirname, basename)
317 }
318
319 if _, ok := fileMap[name]; ok {
320 continue
321 }
322
323
324 if inWhiteoutDir(fileMap, name) {
325 continue
326 }
327
328
329
330 fileMap[name] = tombstone || !(header.Typeflag == tar.TypeDir)
331 if !tombstone {
332 if err := tarWriter.WriteHeader(header); err != nil {
333 return err
334 }
335 if header.Size > 0 {
336 if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil {
337 return err
338 }
339 }
340 }
341 }
342 }
343 return nil
344 }
345
346 func inWhiteoutDir(fileMap map[string]bool, file string) bool {
347 for {
348 if file == "" {
349 break
350 }
351 dirname := filepath.Dir(file)
352 if file == dirname {
353 break
354 }
355 if val, ok := fileMap[dirname]; ok && val {
356 return true
357 }
358 file = dirname
359 }
360 return false
361 }
362
363 func max(a, b int) int {
364 if a > b {
365 return a
366 }
367 return b
368 }
369
370
371 func Time(img v1.Image, t time.Time) (v1.Image, error) {
372 newImage := empty.Image
373
374 layers, err := img.Layers()
375 if err != nil {
376 return nil, fmt.Errorf("getting image layers: %w", err)
377 }
378
379 ocf, err := img.ConfigFile()
380 if err != nil {
381 return nil, fmt.Errorf("getting original config file: %w", err)
382 }
383
384 addendums := make([]Addendum, max(len(ocf.History), len(layers)))
385 var historyIdx, addendumIdx int
386 for layerIdx := 0; layerIdx < len(layers); addendumIdx, layerIdx = addendumIdx+1, layerIdx+1 {
387 newLayer, err := layerTime(layers[layerIdx], t)
388 if err != nil {
389 return nil, fmt.Errorf("setting layer times: %w", err)
390 }
391
392
393 for ; historyIdx < len(ocf.History); historyIdx++ {
394 addendums[addendumIdx].History = ocf.History[historyIdx]
395
396
397 if ocf.History[historyIdx].EmptyLayer {
398 addendumIdx++
399 continue
400 }
401
402 historyIdx++
403 break
404 }
405 if addendumIdx < len(addendums) {
406 addendums[addendumIdx].Layer = newLayer
407 }
408 }
409
410
411 for ; historyIdx < len(ocf.History); historyIdx, addendumIdx = historyIdx+1, addendumIdx+1 {
412 addendums[addendumIdx].History = ocf.History[historyIdx]
413 }
414
415 newImage, err = Append(newImage, addendums...)
416 if err != nil {
417 return nil, fmt.Errorf("appending layers: %w", err)
418 }
419
420 cf, err := newImage.ConfigFile()
421 if err != nil {
422 return nil, fmt.Errorf("setting config file: %w", err)
423 }
424
425 cfg := cf.DeepCopy()
426
427
428 cfg.Architecture = ocf.Architecture
429 cfg.OS = ocf.OS
430 cfg.OSVersion = ocf.OSVersion
431 cfg.Config = ocf.Config
432
433
434 cfg.Created = v1.Time{Time: t}
435
436 for i, h := range cfg.History {
437 h.Created = v1.Time{Time: t}
438 h.CreatedBy = ocf.History[i].CreatedBy
439 h.Comment = ocf.History[i].Comment
440 h.EmptyLayer = ocf.History[i].EmptyLayer
441
442 h.Author = ""
443 cfg.History[i] = h
444 }
445
446 return ConfigFile(newImage, cfg)
447 }
448
449 func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
450 layerReader, err := layer.Uncompressed()
451 if err != nil {
452 return nil, fmt.Errorf("getting layer: %w", err)
453 }
454 defer layerReader.Close()
455 w := new(bytes.Buffer)
456 tarWriter := tar.NewWriter(w)
457 defer tarWriter.Close()
458
459 tarReader := tar.NewReader(layerReader)
460 for {
461 header, err := tarReader.Next()
462 if errors.Is(err, io.EOF) {
463 break
464 }
465 if err != nil {
466 return nil, fmt.Errorf("reading layer: %w", err)
467 }
468
469 header.ModTime = t
470
471
472 if header.Format == tar.FormatPAX || header.Format == tar.FormatGNU {
473 header.AccessTime = t
474 header.ChangeTime = t
475 }
476
477 if err := tarWriter.WriteHeader(header); err != nil {
478 return nil, fmt.Errorf("writing tar header: %w", err)
479 }
480
481 if header.Typeflag == tar.TypeReg {
482
483 if _, err = io.CopyN(tarWriter, tarReader, header.Size); err != nil {
484 return nil, fmt.Errorf("writing layer file: %w", err)
485 }
486 }
487 }
488
489 if err := tarWriter.Close(); err != nil {
490 return nil, err
491 }
492
493 b := w.Bytes()
494
495 opener := func() (io.ReadCloser, error) {
496 return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil
497 }
498 layer, err = tarball.LayerFromOpener(opener)
499 if err != nil {
500 return nil, fmt.Errorf("creating layer: %w", err)
501 }
502
503 return layer, nil
504 }
505
506
507
508 func Canonical(img v1.Image) (v1.Image, error) {
509
510 created := time.Time{}
511 img, err := Time(img, created)
512 if err != nil {
513 return nil, err
514 }
515
516 cf, err := img.ConfigFile()
517 if err != nil {
518 return nil, err
519 }
520
521
522 cfg := cf.DeepCopy()
523
524 cfg.Container = ""
525 cfg.Config.Hostname = ""
526 cfg.DockerVersion = ""
527
528 return ConfigFile(img, cfg)
529 }
530
531
532 func MediaType(img v1.Image, mt types.MediaType) v1.Image {
533 return &image{
534 base: img,
535 mediaType: &mt,
536 }
537 }
538
539
540
541
542 func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image {
543 return &image{
544 base: img,
545 configMediaType: &mt,
546 }
547 }
548
549
550 func IndexMediaType(idx v1.ImageIndex, mt types.MediaType) v1.ImageIndex {
551 return &index{
552 base: idx,
553 mediaType: &mt,
554 }
555 }
556
View as plain text