-
Notifications
You must be signed in to change notification settings - Fork 14
/
fetch.go
267 lines (201 loc) · 8.06 KB
/
fetch.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
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package artifacts
import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/sigstore/cosign/v2/pkg/cosign"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
type imageHandler func(ctx context.Context, logger *zap.Logger, img v1.Image) error
// imageExportHandler exports the image for further processing.
func imageExportHandler(exportHandler func(logger *zap.Logger, r io.Reader) error) imageHandler {
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
logger.Info("extracting the image")
r, w := io.Pipe()
var eg errgroup.Group
eg.Go(func() error {
defer w.Close() //nolint:errcheck
return crane.Export(img, w)
})
eg.Go(func() error {
err := exportHandler(logger, r)
if err != nil {
r.CloseWithError(err) // signal the exporter to stop
}
return err
})
if err := eg.Wait(); err != nil {
return fmt.Errorf("error extracting the image: %w", err)
}
return nil
}
}
// imageOCIHandler exports the image to the OCI format.
func imageOCIHandler(path string) imageHandler {
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("error removing the directory %q: %w", path, err)
}
l, err := layout.Write(path, empty.Index)
if err != nil {
return fmt.Errorf("error creating layout: %w", err)
}
logger.Info("exporting the image", zap.String("destination", path))
if err = l.AppendImage(img); err != nil {
return fmt.Errorf("error exporting the image: %w", err)
}
return nil
}
}
// fetchImageByTag contains combined logic of image handling: heading, downloading, verifying signatures, and exporting.
func (m *Manager) fetchImageByTag(imageName, tag string, architecture Arch, imageHandler imageHandler) error {
// set a timeout for fetching, but don't bind it to any context, as we want fetch operation to finish
ctx, cancel := context.WithTimeout(context.Background(), FetchTimeout)
defer cancel()
// light check first - if the image exists, and resolve the digest
// it's important to do further checks by digest exactly
repoRef := m.imageRegistry.Repo(imageName).Tag(tag)
m.logger.Debug("heading the image", zap.Stringer("image", repoRef))
descriptor, err := m.pullers[architecture].Head(ctx, repoRef)
if err != nil {
return err
}
digestRef := repoRef.Digest(descriptor.Digest.String())
return m.fetchImageByDigest(digestRef, architecture, imageHandler)
}
// fetchImageByDigest fetches an image by digest, verifies signatures, and exports it to the storage.
func (m *Manager) fetchImageByDigest(digestRef name.Digest, architecture Arch, imageHandler imageHandler) error {
var err error
// set a timeout for fetching, but don't bind it to any context, as we want fetch operation to finish
ctx, cancel := context.WithTimeout(context.Background(), FetchTimeout)
defer cancel()
logger := m.logger.With(zap.Stringer("image", digestRef))
// verify the image signature, we only accept properly signed images
logger.Debug("verifying image signature")
_, bundleVerified, method, err := verifyImageSignatures(ctx, digestRef, m.options.ImageVerifyOptions)
if err != nil {
return fmt.Errorf("failed to verify image signature for %s: %w", digestRef.Name(), err)
}
logger.Info("image signature verified", zap.String("verification_method", method), zap.Bool("bundle_verified", bundleVerified))
// pull down the image and extract the necessary parts
logger.Info("pulling the image")
desc, err := m.pullers[architecture].Get(ctx, digestRef)
if err != nil {
return fmt.Errorf("error pulling image %s: %w", digestRef, err)
}
img, err := desc.Image()
if err != nil {
return fmt.Errorf("error creating image from descriptor: %w", err)
}
return imageHandler(ctx, logger, img)
}
// fetchImager fetches 'imager' container, and saves to the storage path.
func (m *Manager) fetchImager(tag string) error {
destinationPath := filepath.Join(m.storagePath, tag)
if err := m.fetchImageByTag(ImagerImage, tag, ArchAmd64, imageExportHandler(func(logger *zap.Logger, r io.Reader) error {
return untar(logger, r, destinationPath+tmpSuffix)
})); err != nil {
return err
}
return os.Rename(destinationPath+tmpSuffix, destinationPath)
}
// fetchExtensionImage fetches a specified extension image and exports it to the storage as OCI.
func (m *Manager) fetchExtensionImage(arch Arch, ref ExtensionRef, destPath string) error {
imageRef := m.imageRegistry.Repo(ref.TaggedReference.RepositoryStr()).Digest(ref.Digest)
if err := m.fetchImageByDigest(imageRef, arch, imageOCIHandler(destPath+tmpSuffix)); err != nil {
return err
}
return os.Rename(destPath+tmpSuffix, destPath)
}
// fetchOverlayImage fetches a specified overlay image and exports it to the storage as OCI.
func (m *Manager) fetchOverlayImage(arch Arch, ref OverlayRef, destPath string) error {
imageRef := m.imageRegistry.Repo(ref.TaggedReference.RepositoryStr()).Digest(ref.Digest)
if err := m.fetchImageByDigest(imageRef, arch, imageOCIHandler(destPath+tmpSuffix)); err != nil {
return err
}
return os.Rename(destPath+tmpSuffix, destPath)
}
// fetchInstallerImage fetches a Talos installer image and exports it to the storage.
func (m *Manager) fetchInstallerImage(arch Arch, versionTag string, destPath string) error {
if err := m.fetchImageByTag(InstallerImage, versionTag, arch, imageOCIHandler(destPath+tmpSuffix)); err != nil {
return err
}
return os.Rename(destPath+tmpSuffix, destPath)
}
func untar(logger *zap.Logger, r io.Reader, destination string) error {
const usrInstallPrefix = "usr/install/"
tr := tar.NewReader(r)
size := int64(0)
for {
hdr, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return fmt.Errorf("error reading tar header: %w", err)
}
if hdr.Typeflag != tar.TypeReg || !strings.HasPrefix(hdr.Name, usrInstallPrefix) { // skip
_, err = io.Copy(io.Discard, tr)
if err != nil {
return fmt.Errorf("error skipping data: %w", err)
}
continue
}
destPath := filepath.Join(destination, hdr.Name[len(usrInstallPrefix):])
if err = os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
return fmt.Errorf("error creating directory %q: %w", filepath.Dir(destPath), err)
}
f, err := os.Create(destPath)
if err != nil {
return fmt.Errorf("error creating file %q: %w", destPath, err)
}
_, err = io.Copy(f, tr)
if err != nil {
return fmt.Errorf("error copying data to %q: %w", destPath, err)
}
if err = f.Close(); err != nil {
return fmt.Errorf("error closing %q: %w", destPath, err)
}
size += hdr.Size
}
logger.Info("extracted the image", zap.Int64("size", size), zap.String("destination", destination))
return nil
}
// Try to verify the image signature with the given verification options. Return the first option
// that worked, if any. Only the last encountered error will be returned.
func verifyImageSignatures(ctx context.Context, digestRef name.Reference, imageVerifyOptions []cosign.CheckOpts) (*cosign.CheckOpts, bool, string, error) {
var multiErr error
if len(imageVerifyOptions) == 0 {
return nil, false, "", errors.New("no verification options provided")
}
for _, ivo := range imageVerifyOptions {
_, bundleVerified, err := cosign.VerifyImageSignatures(ctx, digestRef, &ivo)
if err == nil {
// determine verification method
var verificationMethod string
if ivo.SigVerifier != nil {
verificationMethod = "public key"
} else {
verificationMethod = "certificate subject"
}
return &ivo, bundleVerified, verificationMethod, nil
}
multiErr = errors.Join(multiErr, err)
}
// error will be not nil
return &cosign.CheckOpts{}, false, "", multiErr
}