/
oci.go
351 lines (312 loc) · 11.8 KB
/
oci.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
347
348
349
350
351
package ociutil
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"sync/atomic"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/lf-edge/eve-libs/zedUpload/types"
"github.com/sirupsen/logrus"
)
// Tags return all known tags for a given repository on a given registry.
// Optionally, can use authentication of username and apiKey as provided, else defaults
// to the local user config. Also can use a given http client, else uses the default.
// Returns a slice of tags of the repo passed to it, and error, if any.
func Tags(registry, repository, username, apiKey string, client *http.Client, prgchan types.StatsNotifChan) ([]string, error) {
var (
tags []string
err error
image = fmt.Sprintf("%s/%s", registry, repository)
)
repo, err := name.NewRepository(image)
if err != nil {
return nil, fmt.Errorf("parsing reference %q: %v", image, err)
}
opts := options(username, apiKey, client)
tags, err = remote.List(repo, opts...)
if err != nil {
return nil, fmt.Errorf("error listing tags: %v", err)
}
return tags, nil
}
// Manifest retrieves the manifest for a repo from a registry and returns it.
// Optionally, can use authentication of username and apiKey as provided, else defaults
// to the local user config. Also can use a given http client, else uses the default.
// Returns the manifest of the repo passed to it, the manifest of the resolved image,
// which either is the same as the repo manifest if an image, or the repo resolved
// from a manifest index, the size of the entire image, and error, if any.
func Manifest(registry, repo, username, apiKey string, client *http.Client, prgchan types.StatsNotifChan) ([]byte, []byte, int64, error) {
var (
manifestDirect, manifestResolved []byte
size int64
err error
image = fmt.Sprintf("%s/%s", registry, repo)
)
opts := options(username, apiKey, client)
_, _, _, manifestDirect, manifestResolved, size, err = manifestsDescImg(image, opts)
return manifestDirect, manifestResolved, size, err
}
// PullBlob downloads a blob from a registry and save it as a file as-is.
func PullBlob(registry, repo, hash, localFile, username, apiKey string, maxsize int64, client *http.Client, prgchan types.StatsNotifChan) (int64, string, error) {
logrus.Infof("PullBlob(%s, %s, %s) to %s", registry, repo, hash, localFile)
var (
w io.Writer
r io.Reader
stats types.UpdateStats
size int64
finalErr error
contentType string
)
opts := options(username, apiKey, client)
// The OCI distribution spec only uses /blobs/ endpoint for layers or config, not index or manifest.
// I have no idea why you cannot get a manifest or index from the /blobs endpoint, but so be it.
image := fmt.Sprintf("%s/%s", registry, repo)
ref, err := name.ParseReference(image)
if err != nil {
return 0, "", fmt.Errorf("parsing reference %q: %v", image, err)
}
// If hash is not empty:
// if ref is of type Tag then add hash to the image
// if ref is of type Digest, check if the given hash and the hash in reference are same
if hash != "" {
hash = checkAndCorrectHash(hash)
if _, ok := ref.(name.Tag); ok {
logrus.Infof("PullBlob: Adding hash %s to image %s", hash, image)
image = fmt.Sprintf("%s@%s", image, hash)
ref, err = name.ParseReference(image)
if err != nil {
return 0, "", fmt.Errorf("parsing reference %q: %v", image, err)
}
} else {
d, ok := ref.(name.Digest)
if !ok {
return 0, "", fmt.Errorf("ref %s wasn't a tag or digest", image)
}
if checkAndCorrectHash(d.DigestStr()) != hash {
return 0, "", fmt.Errorf("PullBlob: given hash %s is different from the hash in reference %s",
hash, checkAndCorrectHash(d.DigestStr()))
}
}
}
logrus.Infof("PullBlob(%s): trying to fetch manifest", image)
// check if we have a manifest
r, contentType, size, err = ociGetManifest(ref, opts)
if err != nil {
logrus.Infof("PullBlob(%s): unable to fetch manifest (%s), trying blob", image, err.Error())
// if we have a hash try to get the actual layer
d, ok := ref.(name.Digest)
if !ok {
return 0, "", fmt.Errorf("ref %s wasn't a tag or digest", image)
}
logrus.Infof("PullBlob: had hash, so pulling blob for %s", image)
layer, err := remote.Layer(d, opts...)
if err != nil {
return 0, "", fmt.Errorf("could not pull layer %s: %v", ref.String(), err)
}
// write the layer out to the file
lr, err := layer.Compressed()
if err != nil {
return 0, "", fmt.Errorf("could not get layer reader %s: %v", ref.String(), err)
} else {
defer lr.Close()
r = lr
}
size, err = layer.Size()
if err != nil {
return 0, "", fmt.Errorf("could not get layer size %s: %v", ref.String(), err)
}
}
// check size in case of provided maxsize
if maxsize != 0 && size > maxsize {
return 0, "", fmt.Errorf("actual size of blob (%s, %s, %s) %d is more than provided %d",
registry, repo, hash, size, maxsize)
}
// send out the size
stats.Size = size
types.SendStats(prgchan, stats)
if localFile != "" {
f, err := os.Create(localFile)
if err != nil {
return 0, "", fmt.Errorf("could not open local file %s for writing from %s: %v", localFile, ref.String(), err)
}
defer f.Close()
w = f
} else {
w = os.Stdout
}
// get updates on downloads, convert and pass them to sendStats
c := make(chan Update, 200)
defer close(c)
// copy from the readstream over the network to the writestream to the local file
// we do this in a goroutine so we can catch the updates
pw := &ProgressWriter{
w: w,
updates: c,
size: size,
}
go func() {
// copy all of the data
size, err := io.Copy(pw, r)
if err != nil && err != io.EOF {
logrus.Errorf("could not write to local file %s from %s: %v", localFile, ref.String(), err)
}
if err == nil {
err = io.EOF
}
c <- Update{
Total: pw.size,
Complete: size,
Error: err,
}
}()
for update := range c {
atomic.StoreInt64(&stats.Asize, update.Complete)
atomic.StoreInt64(&stats.Size, update.Total)
types.SendStats(prgchan, stats)
size = update.Complete
// any error means to stop
if update.Error != nil {
// EOF means we are at the end cleanly
if update.Error == io.EOF {
logrus.Infof("PullBlob(%s): download complete to %s size %d", image, localFile, size)
finalErr = nil
} else {
logrus.Errorf("PullBlob(%s): error saving to %s: %v", image, localFile, update.Error)
finalErr = update.Error
}
break
}
}
logrus.Infof("PullBlob(%s): Done. Size: %d, ContentType: %s FinalErr: %v", image, size, contentType, finalErr)
return size, contentType, finalErr
}
// ociGetManifest get an OCI manifest
func ociGetManifest(ref name.Reference, opts []remote.Option) (io.Reader, string, int64, error) {
desc, err := remote.Get(ref, opts...)
if err != nil {
return nil, "", 0, fmt.Errorf("error getting manifest: %v", err)
}
return bytes.NewReader(desc.Manifest), string(desc.MediaType), desc.Size, nil
}
// Pull downloads an entire image from a registry and saves it as a tar file at the provided location.
// Optionally, can use authentication of username and apiKey as provided, else defaults
// to the local user config. Also can use a given http client, else uses the default.
// Returns the manifest of the repo passed to it, the manifest of the resolved image,
// which either is the same as the repo manifest if an image, or the repo resolved
// from a manifest index, the size of the entire download, and error, if any.
func Pull(registry, repo, localFile, username, apiKey string, client *http.Client, prgchan types.StatsNotifChan) ([]byte, []byte, int64, error) {
// this is the manifest referenced by the image. If it is an index, it returns the index.
var (
manifestDirect, manifestResolved []byte
img v1.Image
size int64
err error
ref name.Reference
stats types.UpdateStats
image = fmt.Sprintf("%s/%s", registry, repo)
)
logrus.Infof("Pull(%s, %s) to %s", registry, repo, localFile)
opts := options(username, apiKey, client)
ref, _, img, manifestDirect, manifestResolved, size, err = manifestsDescImg(image, opts)
if err != nil {
return manifestDirect, manifestResolved, size, err
}
// record the target size and send it
stats.Size = size
types.SendStats(prgchan, stats)
// create our local file and save to it
localDir := path.Dir(localFile)
err = os.MkdirAll(localDir, 0755)
if err != nil {
return manifestDirect, manifestResolved, size, fmt.Errorf("unable to create directory to store downloaded file %s: %v", localDir, err)
}
w, err := os.Create(localFile)
if err != nil {
return manifestDirect, manifestResolved, size, err
}
defer w.Close()
tag, ok := ref.(name.Tag)
if !ok {
d, ok := ref.(name.Digest)
if !ok {
err := fmt.Errorf("Image name %s doesn't have a tag or digest", ref)
return manifestDirect, manifestResolved, size, err
}
parts := strings.Split(d.DigestStr(), ":")
if len(parts) != 2 {
err := fmt.Errorf("Image name %s is malformed, expected: <name>@sha256:<hash>", d.String())
return manifestDirect, manifestResolved, size, err
}
digestTag := fmt.Sprintf("dummyTag-%s", parts[1])
tag = d.Repository.Tag(digestTag)
}
// get updates on downloads, convert and pass them to sendStats
c := make(chan v1.Update, 200)
defer close(c)
// create a local file to write the output
// this uses the v1/tarball to write it, which is fully compatible with docker save.
// However, it is missing the "repositories" file, so we add it.
// Eventually, we may want to move to an entire cache of the registry in the
// OCI layout format.
go func() {
// we do not need to catch the return error, because tarball.WithProgress sends error updates on channels
_ = tarball.Write(tag, img, w, tarball.WithProgress(c))
}()
for update := range c {
atomic.StoreInt64(&stats.Asize, update.Complete)
types.SendStats(prgchan, stats)
// EOF means we are at the end
if update.Error != nil && update.Error == io.EOF {
break
}
if update.Error != nil {
return manifestDirect, manifestResolved, size, fmt.Errorf("error saving to %s: %v", localFile, update.Error)
}
}
return manifestDirect, manifestResolved, size, nil
}
func options(username, apiKey string, client *http.Client) []remote.Option {
// default to anonymous, unless we have auth credentials
auth := authn.Anonymous
// do we have auth to use?
if username != "" || apiKey != "" {
auth = authn.FromConfig(authn.AuthConfig{Username: username, Password: apiKey})
}
return []remote.Option{
remote.WithAuth(auth),
remote.WithTransport(client.Transport),
}
}
// LayersFromManifest get the descriptors for layers from a raw image manifest
func LayersFromManifest(imageManifest []byte) ([]v1.Descriptor, error) {
manifest, err := v1.ParseManifest(bytes.NewReader(imageManifest))
if err != nil {
return nil, fmt.Errorf("unable to parse manifest: %v", err)
}
return manifest.Layers, nil
}
// DockerHashFromManifest get the sha256 hash as a string from a raw image
// manifest. The "docker hash" is what is used for the image, i.e. the topmost
// layer.
func DockerHashFromManifest(imageManifest []byte) (string, error) {
layers, err := LayersFromManifest(imageManifest)
if err != nil {
return "", fmt.Errorf("unable to get layers: %v", err)
}
if len(layers) < 1 {
return "", fmt.Errorf("no layers found")
}
return layers[len(layers)-1].Digest.Hex, nil
}
// checkAndCorrectHash prepends algo "sha256:" if not already present.
func checkAndCorrectHash(hash string) string {
return fmt.Sprintf("sha256:%s", strings.TrimPrefix(hash, "sha256:"))
}