crossplane
158 строк · 4.5 Кб
1/*
2Copyright 2023 The Crossplane Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package xpkg
18
19import (
20"archive/tar"
21"bytes"
22"fmt"
23"io"
24"os"
25
26v1 "github.com/google/go-containerregistry/pkg/v1"
27"github.com/google/go-containerregistry/pkg/v1/empty"
28"github.com/google/go-containerregistry/pkg/v1/mutate"
29"github.com/google/go-containerregistry/pkg/v1/tarball"
30
31"github.com/crossplane/crossplane-runtime/pkg/errors"
32)
33
34// Error strings.
35const (
36errLayer = "cannot get image layers"
37errDigest = "cannot get image digest"
38)
39
40// Layer creates a v1.Layer that represents the layer contents for the xpkg and
41// adds a corresponding label to the image Config for the layer.
42func Layer(r io.Reader, fileName, annotation string, fileSize int64, mode os.FileMode, cfg *v1.Config) (v1.Layer, error) {
43tarBuf := new(bytes.Buffer)
44tw := tar.NewWriter(tarBuf)
45
46exHdr := &tar.Header{
47Name: fileName,
48Mode: int64(mode),
49Size: fileSize,
50}
51
52if err := writeLayer(tw, exHdr, r); err != nil {
53return nil, err
54}
55
56// TODO(hasheddan): we currently return a new reader every time here in
57// order to calculate digest, then subsequently write contents to disk. We
58// can greatly improve performance during package build by avoiding reading
59// every layer into memory.
60layer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) {
61return io.NopCloser(bytes.NewReader(tarBuf.Bytes())), nil
62})
63if err != nil {
64return nil, errors.Wrap(err, errLayerFromTar)
65}
66
67d, err := layer.Digest()
68if err != nil {
69return nil, errors.Wrap(err, errDigestInvalid)
70}
71
72// Add annotation label to config if a non-empty label is specified. This is
73// an intermediary step. AnnotateLayers must be called on an image for it to
74// have valid layer annotations. It propagates these labels to annotations
75// on the layers.
76if annotation != "" {
77cfg.Labels[Label(d.String())] = annotation
78}
79
80return layer, nil
81}
82
83func writeLayer(tw *tar.Writer, hdr *tar.Header, buf io.Reader) error {
84if err := tw.WriteHeader(hdr); err != nil {
85return errors.Wrap(err, errTarFromStream)
86}
87
88if _, err := io.Copy(tw, buf); err != nil {
89return errors.Wrap(err, errTarFromStream)
90}
91if err := tw.Close(); err != nil {
92return errors.Wrap(err, errTarFromStream)
93}
94return nil
95}
96
97// Label constructs a specially formated label using the annotationKey.
98func Label(annotation string) string {
99return fmt.Sprintf("%s:%s", AnnotationKey, annotation)
100}
101
102// NOTE(negz): AnnotateLayers originated in upbound/up. I was confused why we
103// store layer annotations as labels in the OCI config file when we build a
104// package, then propagate them to OCI layer annotations when we push one. I
105// believe this is because an xpkg file is really an OCI image tarball, and the
106// tarball format doesn't support layer annotations (or may just lose them in
107// some circumstances?), so we're using the config file to store them.
108// See https://github.com/upbound/up/pull/177#discussion_r866776584.
109
110// AnnotateLayers propagates labels from the supplied image's config file to
111// annotations on its layers.
112func AnnotateLayers(i v1.Image) (v1.Image, error) {
113cfgFile, err := i.ConfigFile()
114if err != nil {
115return nil, errors.Wrap(err, errConfigFile)
116}
117
118layers, err := i.Layers()
119if err != nil {
120return nil, errors.Wrap(err, errLayer)
121}
122
123addendums := make([]mutate.Addendum, 0)
124
125for _, l := range layers {
126d, err := l.Digest()
127if err != nil {
128return nil, errors.Wrap(err, errDigest)
129}
130if annotation, ok := cfgFile.Config.Labels[Label(d.String())]; ok {
131addendums = append(addendums, mutate.Addendum{
132Layer: l,
133Annotations: map[string]string{
134AnnotationKey: annotation,
135},
136})
137continue
138}
139addendums = append(addendums, mutate.Addendum{
140Layer: l,
141})
142}
143
144// we didn't find any annotations, return original image
145if len(addendums) == 0 {
146return i, nil
147}
148
149img := empty.Image
150for _, a := range addendums {
151img, err = mutate.Append(img, a)
152if err != nil {
153return nil, errors.Wrap(err, errBuildImage)
154}
155}
156
157return mutate.ConfigFile(img, cfgFile)
158}
159