inspektor-gadget

Форк
0
612 строк · 18.4 Кб
1
// Copyright 2023 The Inspektor Gadget authors
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
package oci
16

17
import (
18
	"bytes"
19
	"context"
20
	"encoding/json"
21
	"errors"
22
	"fmt"
23
	"io"
24
	"os"
25
	"path/filepath"
26
	"runtime"
27
	"strings"
28

29
	"github.com/distribution/reference"
30
	"github.com/docker/cli/cli/config"
31
	"github.com/docker/cli/cli/config/configfile"
32
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
33
	log "github.com/sirupsen/logrus"
34
	"oras.land/oras-go/v2"
35
	"oras.land/oras-go/v2/content/oci"
36
	"oras.land/oras-go/v2/errdef"
37
	"oras.land/oras-go/v2/registry/remote"
38
	oras_auth "oras.land/oras-go/v2/registry/remote/auth"
39
)
40

41
type AuthOptions struct {
42
	AuthFile    string
43
	SecretBytes []byte
44
	Insecure    bool
45
}
46

47
const (
48
	defaultOciStore = "/var/lib/ig/oci-store"
49
	DefaultAuthFile = "/var/lib/ig/config.json"
50

51
	PullImageAlways  = "always"
52
	PullImageMissing = "missing"
53
	PullImageNever   = "never"
54
)
55

56
const (
57
	defaultDomain      = "ghcr.io"
58
	officialRepoPrefix = "inspektor-gadget/gadget/"
59
	// localhost is treated as a special value for domain-name. Any other
60
	// domain-name without a "." or a ":port" are considered a path component.
61
	localhost = "localhost"
62
)
63

64
// GadgetImage is the representation of a gadget packaged in an OCI image.
65
type GadgetImage struct {
66
	EbpfObject []byte
67
	WasmObject []byte
68
	Metadata   []byte
69
}
70

71
// GadgetImageDesc is the description of a gadget image.
72
type GadgetImageDesc struct {
73
	Repository string `column:"repository"`
74
	Tag        string `column:"tag"`
75
	Digest     string `column:"digest,width:12,fixed"`
76
}
77

78
func (d *GadgetImageDesc) String() string {
79
	if d.Tag == "" && d.Repository == "" {
80
		return fmt.Sprintf("@%s", d.Digest)
81
	}
82
	return fmt.Sprintf("%s:%s@%s", d.Repository, d.Tag, d.Digest)
83
}
84

85
func getLocalOciStore() (*oci.Store, error) {
86
	if err := os.MkdirAll(filepath.Dir(defaultOciStore), 0o700); err != nil {
87
		return nil, err
88
	}
89
	return oci.New(defaultOciStore)
90
}
91

92
// GetGadgetImage pulls the gadget image according to the pull policy and returns
93
// a GadgetImage structure representing it.
94
func GetGadgetImage(ctx context.Context, image string, authOpts *AuthOptions, pullPolicy string) (*GadgetImage, error) {
95
	imageStore, err := getLocalOciStore()
96
	if err != nil {
97
		return nil, fmt.Errorf("getting local oci store: %w", err)
98
	}
99

100
	switch pullPolicy {
101
	case PullImageAlways:
102
		_, err := pullGadgetImageToStore(ctx, imageStore, image, authOpts)
103
		if err != nil {
104
			return nil, fmt.Errorf("pulling image %q: %w", image, err)
105
		}
106
	case PullImageMissing:
107
		if err := pullIfNotExist(ctx, imageStore, authOpts, image); err != nil {
108
			return nil, fmt.Errorf("pulling image %q: %w", image, err)
109
		}
110
	case PullImageNever:
111
		// Just check if the image exists to report a better error message
112
		targetImage, err := normalizeImageName(image)
113
		if err != nil {
114
			return nil, fmt.Errorf("normalizing image: %w", err)
115
		}
116
		if _, err := imageStore.Resolve(ctx, targetImage.String()); err != nil {
117
			return nil, fmt.Errorf("resolving image %q on local registry: %w", targetImage.String(), err)
118
		}
119
	}
120

121
	manifest, err := getImageManifest(ctx, imageStore, image, authOpts)
122
	if err != nil {
123
		return nil, fmt.Errorf("getting arch manifest: %w", err)
124
	}
125

126
	prog, err := getLayerFromManifest(ctx, imageStore, manifest, eBPFObjectMediaType)
127
	if err != nil {
128
		return nil, fmt.Errorf("getting ebpf program: %w", err)
129
	}
130
	if prog == nil {
131
		return nil, fmt.Errorf("no ebpf program found")
132
	}
133

134
	wasm, err := getLayerFromManifest(ctx, imageStore, manifest, wasmObjectMediaType)
135
	if err != nil {
136
		return nil, fmt.Errorf("getting wasm program: %w", err)
137
	}
138

139
	metadata, err := getMetadataFromManifest(ctx, imageStore, manifest)
140
	if err != nil {
141
		return nil, fmt.Errorf("getting metadata: %w", err)
142
	}
143

144
	return &GadgetImage{
145
		EbpfObject: prog,
146
		WasmObject: wasm,
147
		Metadata:   metadata,
148
	}, nil
149
}
150

151
// PullGadgetImage pulls the gadget image into the local oci store and returns its descriptor.
152
func PullGadgetImage(ctx context.Context, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) {
153
	ociStore, err := getLocalOciStore()
154
	if err != nil {
155
		return nil, fmt.Errorf("getting oci store: %w", err)
156
	}
157

158
	return pullGadgetImageToStore(ctx, ociStore, image, authOpts)
159
}
160

161
// pullGadgetImageToStore pulls the gadget image into the given store and returns its descriptor.
162
func pullGadgetImageToStore(ctx context.Context, imageStore oras.Target, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) {
163
	targetImage, err := normalizeImageName(image)
164
	if err != nil {
165
		return nil, fmt.Errorf("normalizing image: %w", err)
166
	}
167
	repo, err := newRepository(targetImage, authOpts)
168
	if err != nil {
169
		return nil, fmt.Errorf("creating remote repository: %w", err)
170
	}
171
	desc, err := oras.Copy(ctx, repo, targetImage.String(), imageStore,
172
		targetImage.String(), oras.DefaultCopyOptions)
173
	if err != nil {
174
		return nil, fmt.Errorf("copying to remote repository: %w", err)
175
	}
176

177
	imageDesc := &GadgetImageDesc{
178
		Repository: targetImage.Name(),
179
		Digest:     desc.Digest.String(),
180
	}
181
	if ref, ok := targetImage.(reference.Tagged); ok {
182
		imageDesc.Tag = ref.Tag()
183
	}
184
	return imageDesc, nil
185
}
186

187
func pullIfNotExist(ctx context.Context, imageStore oras.Target, authOpts *AuthOptions, image string) error {
188
	targetImage, err := normalizeImageName(image)
189
	if err != nil {
190
		return fmt.Errorf("normalizing image: %w", err)
191
	}
192

193
	_, err = imageStore.Resolve(ctx, targetImage.String())
194
	if err == nil {
195
		return nil
196
	}
197
	if !errors.Is(err, errdef.ErrNotFound) {
198
		return fmt.Errorf("resolving image %q: %w", image, err)
199
	}
200

201
	repo, err := newRepository(targetImage, authOpts)
202
	if err != nil {
203
		return fmt.Errorf("creating remote repository: %w", err)
204
	}
205
	_, err = oras.Copy(ctx, repo, targetImage.String(), imageStore, targetImage.String(), oras.DefaultCopyOptions)
206
	if err != nil {
207
		return fmt.Errorf("downloading to local repository: %w", err)
208
	}
209
	return nil
210
}
211

212
// PushGadgetImage pushes the gadget image and returns its descriptor.
213
func PushGadgetImage(ctx context.Context, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) {
214
	ociStore, err := getLocalOciStore()
215
	if err != nil {
216
		return nil, fmt.Errorf("getting oci store: %w", err)
217
	}
218

219
	targetImage, err := normalizeImageName(image)
220
	if err != nil {
221
		return nil, fmt.Errorf("normalizing image: %w", err)
222
	}
223
	repo, err := newRepository(targetImage, authOpts)
224
	if err != nil {
225
		return nil, fmt.Errorf("creating remote repository: %w", err)
226
	}
227
	desc, err := oras.Copy(context.TODO(), ociStore, targetImage.String(), repo,
228
		targetImage.String(), oras.DefaultCopyOptions)
229
	if err != nil {
230
		return nil, fmt.Errorf("copying to remote repository: %w", err)
231
	}
232

233
	imageDesc := &GadgetImageDesc{
234
		Repository: targetImage.Name(),
235
		Digest:     desc.Digest.String(),
236
	}
237
	if ref, ok := targetImage.(reference.Tagged); ok {
238
		imageDesc.Tag = ref.Tag()
239
	}
240
	return imageDesc, nil
241
}
242

243
// TagGadgetImage tags the src image with the dst image.
244
func TagGadgetImage(ctx context.Context, srcImage, dstImage string) (*GadgetImageDesc, error) {
245
	src, err := normalizeImageName(srcImage)
246
	if err != nil {
247
		return nil, fmt.Errorf("normalizing src image: %w", err)
248
	}
249
	dst, err := normalizeImageName(dstImage)
250
	if err != nil {
251
		return nil, fmt.Errorf("normalizing dst image: %w", err)
252
	}
253

254
	ociStore, err := getLocalOciStore()
255
	if err != nil {
256
		return nil, fmt.Errorf("getting oci store: %w", err)
257
	}
258

259
	targetDescriptor, err := ociStore.Resolve(context.TODO(), src.String())
260
	if err != nil {
261
		// Error message not that helpful
262
		return nil, fmt.Errorf("resolving src: %w", err)
263
	}
264
	ociStore.Tag(context.TODO(), targetDescriptor, dst.String())
265

266
	imageDesc := &GadgetImageDesc{
267
		Repository: dst.Name(),
268
		Digest:     targetDescriptor.Digest.String(),
269
	}
270
	if ref, ok := dst.(reference.Tagged); ok {
271
		imageDesc.Tag = ref.Tag()
272
	}
273
	return imageDesc, nil
274
}
275

276
func listGadgetImages(ctx context.Context, store *oci.Store) ([]*GadgetImageDesc, error) {
277
	images := []*GadgetImageDesc{}
278
	err := store.Tags(ctx, "", func(tags []string) error {
279
		for _, fullTag := range tags {
280
			parsed, err := reference.Parse(fullTag)
281
			if err != nil {
282
				log.Debugf("parsing image %q: %s", fullTag, err)
283
				continue
284
			}
285

286
			var repository string
287
			if named, ok := parsed.(reference.Named); ok {
288
				repository = named.Name()
289
			}
290

291
			tag := "latest"
292
			if tagged, ok := parsed.(reference.Tagged); ok {
293
				tag = tagged.Tag()
294
			}
295

296
			image := &GadgetImageDesc{
297
				Repository: repository,
298
				Tag:        tag,
299
			}
300

301
			desc, err := store.Resolve(ctx, fullTag)
302
			if err != nil {
303
				log.Debugf("Found tag %q but couldn't get a descriptor for it: %v", fullTag, err)
304
				continue
305
			}
306
			image.Digest = desc.Digest.String()
307
			images = append(images, image)
308
		}
309
		return nil
310
	})
311

312
	return images, err
313
}
314

315
// ListGadgetImages lists all the gadget images.
316
func ListGadgetImages(ctx context.Context) ([]*GadgetImageDesc, error) {
317
	ociStore, err := getLocalOciStore()
318
	if err != nil {
319
		return nil, fmt.Errorf("getting oci store: %w", err)
320
	}
321

322
	images, err := listGadgetImages(ctx, ociStore)
323
	if err != nil {
324
		return nil, fmt.Errorf("listing all tags: %w", err)
325
	}
326

327
	for _, image := range images {
328
		image.Repository = strings.TrimPrefix(image.Repository, defaultDomain+"/"+officialRepoPrefix)
329
	}
330

331
	return images, nil
332
}
333

334
// DeleteGadgetImage removes the given image.
335
func DeleteGadgetImage(ctx context.Context, image string) error {
336
	ociStore, err := getLocalOciStore()
337
	if err != nil {
338
		return fmt.Errorf("getting oci store: %w", err)
339
	}
340

341
	targetImage, err := normalizeImageName(image)
342
	if err != nil {
343
		return fmt.Errorf("normalizing image: %w", err)
344
	}
345

346
	fullName := targetImage.String()
347
	descriptor, err := ociStore.Resolve(ctx, fullName)
348
	if err != nil {
349
		return fmt.Errorf("resolving image: %w", err)
350
	}
351

352
	images, err := listGadgetImages(ctx, ociStore)
353
	if err != nil {
354
		return fmt.Errorf("listing images: %w", err)
355
	}
356

357
	digest := descriptor.Digest.String()
358
	for _, img := range images {
359
		imgFullName := fmt.Sprintf("%s:%s", img.Repository, img.Tag)
360
		if img.Digest == digest && imgFullName != fullName {
361
			// We cannot blindly delete a whole image tree.
362
			// Indeed, it is possible for several image names to point to the same
363
			// underlying image, like:
364
			// REPOSITORY            TAG    DIGEST
365
			// docker.io/library/bar latest f959f580ba01
366
			// docker.io/library/foo latest f959f580ba01
367
			// Where foo and bar are different names referencing the same image, as
368
			// the digest shows.
369
			// In this case, we just untag the image name given by the user.
370
			return ociStore.Untag(ctx, fullName)
371
		}
372
	}
373

374
	err = ociStore.Delete(ctx, descriptor)
375
	if err != nil {
376
		return err
377
	}
378

379
	return ociStore.GC(ctx)
380
}
381

382
// splitIGDomain splits a repository name to domain and remote-name.
383
// If no valid domain is found, the default domain is used. Repository name
384
// needs to be already validated before.
385
// Inspired on https://github.com/distribution/reference/blob/v0.5.0/normalize.go#L126
386
// TODO: Ideally we should use the upstream function but docker.io is harcoded there
387
// https://github.com/distribution/reference/blob/v0.5.0/normalize.go#L31
388
func splitIGDomain(name string) (domain, remainder string) {
389
	i := strings.IndexRune(name, '/')
390
	if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != localhost && strings.ToLower(name[:i]) == name[:i]) {
391
		domain, remainder = defaultDomain, name
392
	} else {
393
		domain, remainder = name[:i], name[i+1:]
394
	}
395
	if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
396
		remainder = officialRepoPrefix + remainder
397
	}
398
	return
399
}
400

401
func normalizeImageName(image string) (reference.Named, error) {
402
	// Use the default gadget's registry if no domain is specified.
403
	domain, remainer := splitIGDomain(image)
404

405
	name, err := reference.ParseNormalizedNamed(domain + "/" + remainer)
406
	if err != nil {
407
		return nil, fmt.Errorf("parsing normalized image %q: %w", image, err)
408
	}
409
	return reference.TagNameOnly(name), nil
410
}
411

412
func getHostString(repository string) (string, error) {
413
	repo, err := reference.Parse(repository)
414
	if err != nil {
415
		return "", fmt.Errorf("parsing repository %q: %w", repository, err)
416
	}
417
	if named, ok := repo.(reference.Named); ok {
418
		return reference.Domain(named), nil
419
	}
420
	return "", fmt.Errorf("image has to be a named reference")
421
}
422

423
func newAuthClient(repository string, authOptions *AuthOptions) (*oras_auth.Client, error) {
424
	log.Debugf("Using auth file %q", authOptions.AuthFile)
425

426
	var cfg *configfile.ConfigFile
427
	var err error
428

429
	if authOptions.SecretBytes != nil && len(authOptions.SecretBytes) != 0 {
430
		cfg, err = config.LoadFromReader(bytes.NewReader(authOptions.SecretBytes))
431
		if err != nil {
432
			return nil, fmt.Errorf("loading auth config: %w", err)
433
		}
434
	} else if authFileReader, err := os.Open(authOptions.AuthFile); err != nil {
435
		// If the AuthFile was not set explicitly, we allow to fall back to the docker auth,
436
		// otherwise we fail to avoid masking an error from the user
437
		if !errors.Is(err, os.ErrNotExist) || authOptions.AuthFile != DefaultAuthFile {
438
			return nil, fmt.Errorf("opening auth file %q: %w", authOptions.AuthFile, err)
439
		}
440

441
		log.Debugf("Couldn't find default auth file %q...", authOptions.AuthFile)
442
		log.Debugf("Using default docker auth file instead")
443
		log.Debugf("$HOME: %q", os.Getenv("HOME"))
444

445
		cfg, err = config.Load("")
446
		if err != nil {
447
			return nil, fmt.Errorf("loading auth config: %w", err)
448
		}
449

450
	} else {
451
		defer authFileReader.Close()
452
		cfg, err = config.LoadFromReader(authFileReader)
453
		if err != nil {
454
			return nil, fmt.Errorf("loading auth config: %w", err)
455
		}
456
	}
457

458
	hostString, err := getHostString(repository)
459
	if err != nil {
460
		return nil, fmt.Errorf("getting host string: %w", err)
461
	}
462
	authConfig, err := cfg.GetAuthConfig(hostString)
463
	if err != nil {
464
		return nil, fmt.Errorf("getting auth config: %w", err)
465
	}
466

467
	return &oras_auth.Client{
468
		Credential: oras_auth.StaticCredential(hostString, oras_auth.Credential{
469
			Username:     authConfig.Username,
470
			Password:     authConfig.Password,
471
			AccessToken:  authConfig.Auth,
472
			RefreshToken: authConfig.IdentityToken,
473
		}),
474
	}, nil
475
}
476

477
// newRepository creates a client to the remote repository identified by
478
// image using the given auth options.
479
func newRepository(image reference.Named, authOpts *AuthOptions) (*remote.Repository, error) {
480
	repo, err := remote.NewRepository(image.Name())
481
	if err != nil {
482
		return nil, fmt.Errorf("creating remote repository: %w", err)
483
	}
484
	repo.PlainHTTP = authOpts.Insecure
485
	if !authOpts.Insecure {
486
		client, err := newAuthClient(image.Name(), authOpts)
487
		if err != nil {
488
			return nil, fmt.Errorf("creating auth client: %w", err)
489
		}
490
		repo.Client = client
491
	}
492

493
	return repo, nil
494
}
495

496
func getImageListDescriptor(ctx context.Context, imageStore oras.ReadOnlyTarget, reference string) (ocispec.Index, error) {
497
	imageListDescriptor, err := imageStore.Resolve(ctx, reference)
498
	if err != nil {
499
		return ocispec.Index{}, fmt.Errorf("resolving image %q: %w", reference, err)
500
	}
501
	if imageListDescriptor.MediaType != ocispec.MediaTypeImageIndex {
502
		return ocispec.Index{}, fmt.Errorf("image %q is not an image index", reference)
503
	}
504

505
	reader, err := imageStore.Fetch(ctx, imageListDescriptor)
506
	if err != nil {
507
		return ocispec.Index{}, fmt.Errorf("fetching image index: %w", err)
508
	}
509
	defer reader.Close()
510

511
	var index ocispec.Index
512
	if err = json.NewDecoder(reader).Decode(&index); err != nil {
513
		return ocispec.Index{}, fmt.Errorf("unmarshalling image index: %w", err)
514
	}
515
	return index, nil
516
}
517

518
func getArchManifest(imageStore oras.ReadOnlyTarget, index ocispec.Index) (*ocispec.Manifest, error) {
519
	var manifestDesc ocispec.Descriptor
520
	for _, indexManifest := range index.Manifests {
521
		// TODO: Check docker code
522
		if indexManifest.Platform.Architecture == runtime.GOARCH {
523
			manifestDesc = indexManifest
524
			break
525
		}
526
	}
527
	if manifestDesc.Digest == "" {
528
		return nil, fmt.Errorf("no manifest found for architecture %q", runtime.GOARCH)
529
	}
530

531
	reader, err := imageStore.Fetch(context.TODO(), manifestDesc)
532
	if err != nil {
533
		return nil, fmt.Errorf("fetching manifest: %w", err)
534
	}
535
	defer reader.Close()
536

537
	var manifest ocispec.Manifest
538
	if err = json.NewDecoder(reader).Decode(&manifest); err != nil {
539
		return nil, fmt.Errorf("unmarshalling manifest: %w", err)
540
	}
541
	return &manifest, nil
542
}
543

544
func getMetadataFromManifest(ctx context.Context, target oras.Target, manifest *ocispec.Manifest) ([]byte, error) {
545
	// metadata is optional
546
	if manifest.Config.Size == 0 {
547
		return nil, nil
548
	}
549

550
	metadata, err := getContentFromDescriptor(ctx, target, manifest.Config)
551
	if err != nil {
552
		return nil, fmt.Errorf("getting metadata from descriptor: %w", err)
553
	}
554

555
	return metadata, nil
556
}
557

558
func getLayerFromManifest(ctx context.Context, target oras.Target, manifest *ocispec.Manifest, mediaType string) ([]byte, error) {
559
	var layer ocispec.Descriptor
560
	layerCount := 0
561
	for _, l := range manifest.Layers {
562
		if l.MediaType == mediaType {
563
			layer = l
564
			layerCount++
565
		}
566
	}
567
	if layerCount == 0 {
568
		return nil, nil
569
	}
570
	if layerCount != 1 {
571
		return nil, fmt.Errorf("expected exactly one layer with media type %q, got %d", mediaType, layerCount)
572
	}
573
	layerBytes, err := getContentFromDescriptor(ctx, target, layer)
574
	if err != nil {
575
		return nil, fmt.Errorf("getting layer %q from descriptor: %w", mediaType, err)
576
	}
577
	if len(layerBytes) == 0 {
578
		return nil, errors.New("layer is empty")
579
	}
580
	return layerBytes, nil
581
}
582

583
func getContentFromDescriptor(ctx context.Context, imageStore oras.ReadOnlyTarget, desc ocispec.Descriptor) ([]byte, error) {
584
	reader, err := imageStore.Fetch(ctx, desc)
585
	if err != nil {
586
		return nil, fmt.Errorf("fetching descriptor: %w", err)
587
	}
588
	defer reader.Close()
589
	bytes, err := io.ReadAll(reader)
590
	if err != nil {
591
		return nil, fmt.Errorf("reading descriptor: %w", err)
592
	}
593
	return bytes, nil
594
}
595

596
func getImageManifest(ctx context.Context, target oras.Target, image string, authOpts *AuthOptions) (*ocispec.Manifest, error) {
597
	imageRef, err := normalizeImageName(image)
598
	if err != nil {
599
		return nil, fmt.Errorf("normalizing image: %w", err)
600
	}
601

602
	index, err := getImageListDescriptor(ctx, target, imageRef.String())
603
	if err != nil {
604
		return nil, fmt.Errorf("getting image list descriptor: %w", err)
605
	}
606

607
	manifestHost, err := getArchManifest(target, index)
608
	if err != nil {
609
		return nil, fmt.Errorf("getting arch manifest: %w", err)
610
	}
611
	return manifestHost, nil
612
}
613

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.