inspektor-gadget
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
15package oci
16
17import (
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"
32ocispec "github.com/opencontainers/image-spec/specs-go/v1"
33log "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"
38oras_auth "oras.land/oras-go/v2/registry/remote/auth"
39)
40
41type AuthOptions struct {
42AuthFile string
43SecretBytes []byte
44Insecure bool
45}
46
47const (
48defaultOciStore = "/var/lib/ig/oci-store"
49DefaultAuthFile = "/var/lib/ig/config.json"
50
51PullImageAlways = "always"
52PullImageMissing = "missing"
53PullImageNever = "never"
54)
55
56const (
57defaultDomain = "ghcr.io"
58officialRepoPrefix = "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.
61localhost = "localhost"
62)
63
64// GadgetImage is the representation of a gadget packaged in an OCI image.
65type GadgetImage struct {
66EbpfObject []byte
67WasmObject []byte
68Metadata []byte
69}
70
71// GadgetImageDesc is the description of a gadget image.
72type GadgetImageDesc struct {
73Repository string `column:"repository"`
74Tag string `column:"tag"`
75Digest string `column:"digest,width:12,fixed"`
76}
77
78func (d *GadgetImageDesc) String() string {
79if d.Tag == "" && d.Repository == "" {
80return fmt.Sprintf("@%s", d.Digest)
81}
82return fmt.Sprintf("%s:%s@%s", d.Repository, d.Tag, d.Digest)
83}
84
85func getLocalOciStore() (*oci.Store, error) {
86if err := os.MkdirAll(filepath.Dir(defaultOciStore), 0o700); err != nil {
87return nil, err
88}
89return oci.New(defaultOciStore)
90}
91
92// GetGadgetImage pulls the gadget image according to the pull policy and returns
93// a GadgetImage structure representing it.
94func GetGadgetImage(ctx context.Context, image string, authOpts *AuthOptions, pullPolicy string) (*GadgetImage, error) {
95imageStore, err := getLocalOciStore()
96if err != nil {
97return nil, fmt.Errorf("getting local oci store: %w", err)
98}
99
100switch pullPolicy {
101case PullImageAlways:
102_, err := pullGadgetImageToStore(ctx, imageStore, image, authOpts)
103if err != nil {
104return nil, fmt.Errorf("pulling image %q: %w", image, err)
105}
106case PullImageMissing:
107if err := pullIfNotExist(ctx, imageStore, authOpts, image); err != nil {
108return nil, fmt.Errorf("pulling image %q: %w", image, err)
109}
110case PullImageNever:
111// Just check if the image exists to report a better error message
112targetImage, err := normalizeImageName(image)
113if err != nil {
114return nil, fmt.Errorf("normalizing image: %w", err)
115}
116if _, err := imageStore.Resolve(ctx, targetImage.String()); err != nil {
117return nil, fmt.Errorf("resolving image %q on local registry: %w", targetImage.String(), err)
118}
119}
120
121manifest, err := getImageManifest(ctx, imageStore, image, authOpts)
122if err != nil {
123return nil, fmt.Errorf("getting arch manifest: %w", err)
124}
125
126prog, err := getLayerFromManifest(ctx, imageStore, manifest, eBPFObjectMediaType)
127if err != nil {
128return nil, fmt.Errorf("getting ebpf program: %w", err)
129}
130if prog == nil {
131return nil, fmt.Errorf("no ebpf program found")
132}
133
134wasm, err := getLayerFromManifest(ctx, imageStore, manifest, wasmObjectMediaType)
135if err != nil {
136return nil, fmt.Errorf("getting wasm program: %w", err)
137}
138
139metadata, err := getMetadataFromManifest(ctx, imageStore, manifest)
140if err != nil {
141return nil, fmt.Errorf("getting metadata: %w", err)
142}
143
144return &GadgetImage{
145EbpfObject: prog,
146WasmObject: wasm,
147Metadata: metadata,
148}, nil
149}
150
151// PullGadgetImage pulls the gadget image into the local oci store and returns its descriptor.
152func PullGadgetImage(ctx context.Context, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) {
153ociStore, err := getLocalOciStore()
154if err != nil {
155return nil, fmt.Errorf("getting oci store: %w", err)
156}
157
158return pullGadgetImageToStore(ctx, ociStore, image, authOpts)
159}
160
161// pullGadgetImageToStore pulls the gadget image into the given store and returns its descriptor.
162func pullGadgetImageToStore(ctx context.Context, imageStore oras.Target, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) {
163targetImage, err := normalizeImageName(image)
164if err != nil {
165return nil, fmt.Errorf("normalizing image: %w", err)
166}
167repo, err := newRepository(targetImage, authOpts)
168if err != nil {
169return nil, fmt.Errorf("creating remote repository: %w", err)
170}
171desc, err := oras.Copy(ctx, repo, targetImage.String(), imageStore,
172targetImage.String(), oras.DefaultCopyOptions)
173if err != nil {
174return nil, fmt.Errorf("copying to remote repository: %w", err)
175}
176
177imageDesc := &GadgetImageDesc{
178Repository: targetImage.Name(),
179Digest: desc.Digest.String(),
180}
181if ref, ok := targetImage.(reference.Tagged); ok {
182imageDesc.Tag = ref.Tag()
183}
184return imageDesc, nil
185}
186
187func pullIfNotExist(ctx context.Context, imageStore oras.Target, authOpts *AuthOptions, image string) error {
188targetImage, err := normalizeImageName(image)
189if err != nil {
190return fmt.Errorf("normalizing image: %w", err)
191}
192
193_, err = imageStore.Resolve(ctx, targetImage.String())
194if err == nil {
195return nil
196}
197if !errors.Is(err, errdef.ErrNotFound) {
198return fmt.Errorf("resolving image %q: %w", image, err)
199}
200
201repo, err := newRepository(targetImage, authOpts)
202if err != nil {
203return fmt.Errorf("creating remote repository: %w", err)
204}
205_, err = oras.Copy(ctx, repo, targetImage.String(), imageStore, targetImage.String(), oras.DefaultCopyOptions)
206if err != nil {
207return fmt.Errorf("downloading to local repository: %w", err)
208}
209return nil
210}
211
212// PushGadgetImage pushes the gadget image and returns its descriptor.
213func PushGadgetImage(ctx context.Context, image string, authOpts *AuthOptions) (*GadgetImageDesc, error) {
214ociStore, err := getLocalOciStore()
215if err != nil {
216return nil, fmt.Errorf("getting oci store: %w", err)
217}
218
219targetImage, err := normalizeImageName(image)
220if err != nil {
221return nil, fmt.Errorf("normalizing image: %w", err)
222}
223repo, err := newRepository(targetImage, authOpts)
224if err != nil {
225return nil, fmt.Errorf("creating remote repository: %w", err)
226}
227desc, err := oras.Copy(context.TODO(), ociStore, targetImage.String(), repo,
228targetImage.String(), oras.DefaultCopyOptions)
229if err != nil {
230return nil, fmt.Errorf("copying to remote repository: %w", err)
231}
232
233imageDesc := &GadgetImageDesc{
234Repository: targetImage.Name(),
235Digest: desc.Digest.String(),
236}
237if ref, ok := targetImage.(reference.Tagged); ok {
238imageDesc.Tag = ref.Tag()
239}
240return imageDesc, nil
241}
242
243// TagGadgetImage tags the src image with the dst image.
244func TagGadgetImage(ctx context.Context, srcImage, dstImage string) (*GadgetImageDesc, error) {
245src, err := normalizeImageName(srcImage)
246if err != nil {
247return nil, fmt.Errorf("normalizing src image: %w", err)
248}
249dst, err := normalizeImageName(dstImage)
250if err != nil {
251return nil, fmt.Errorf("normalizing dst image: %w", err)
252}
253
254ociStore, err := getLocalOciStore()
255if err != nil {
256return nil, fmt.Errorf("getting oci store: %w", err)
257}
258
259targetDescriptor, err := ociStore.Resolve(context.TODO(), src.String())
260if err != nil {
261// Error message not that helpful
262return nil, fmt.Errorf("resolving src: %w", err)
263}
264ociStore.Tag(context.TODO(), targetDescriptor, dst.String())
265
266imageDesc := &GadgetImageDesc{
267Repository: dst.Name(),
268Digest: targetDescriptor.Digest.String(),
269}
270if ref, ok := dst.(reference.Tagged); ok {
271imageDesc.Tag = ref.Tag()
272}
273return imageDesc, nil
274}
275
276func listGadgetImages(ctx context.Context, store *oci.Store) ([]*GadgetImageDesc, error) {
277images := []*GadgetImageDesc{}
278err := store.Tags(ctx, "", func(tags []string) error {
279for _, fullTag := range tags {
280parsed, err := reference.Parse(fullTag)
281if err != nil {
282log.Debugf("parsing image %q: %s", fullTag, err)
283continue
284}
285
286var repository string
287if named, ok := parsed.(reference.Named); ok {
288repository = named.Name()
289}
290
291tag := "latest"
292if tagged, ok := parsed.(reference.Tagged); ok {
293tag = tagged.Tag()
294}
295
296image := &GadgetImageDesc{
297Repository: repository,
298Tag: tag,
299}
300
301desc, err := store.Resolve(ctx, fullTag)
302if err != nil {
303log.Debugf("Found tag %q but couldn't get a descriptor for it: %v", fullTag, err)
304continue
305}
306image.Digest = desc.Digest.String()
307images = append(images, image)
308}
309return nil
310})
311
312return images, err
313}
314
315// ListGadgetImages lists all the gadget images.
316func ListGadgetImages(ctx context.Context) ([]*GadgetImageDesc, error) {
317ociStore, err := getLocalOciStore()
318if err != nil {
319return nil, fmt.Errorf("getting oci store: %w", err)
320}
321
322images, err := listGadgetImages(ctx, ociStore)
323if err != nil {
324return nil, fmt.Errorf("listing all tags: %w", err)
325}
326
327for _, image := range images {
328image.Repository = strings.TrimPrefix(image.Repository, defaultDomain+"/"+officialRepoPrefix)
329}
330
331return images, nil
332}
333
334// DeleteGadgetImage removes the given image.
335func DeleteGadgetImage(ctx context.Context, image string) error {
336ociStore, err := getLocalOciStore()
337if err != nil {
338return fmt.Errorf("getting oci store: %w", err)
339}
340
341targetImage, err := normalizeImageName(image)
342if err != nil {
343return fmt.Errorf("normalizing image: %w", err)
344}
345
346fullName := targetImage.String()
347descriptor, err := ociStore.Resolve(ctx, fullName)
348if err != nil {
349return fmt.Errorf("resolving image: %w", err)
350}
351
352images, err := listGadgetImages(ctx, ociStore)
353if err != nil {
354return fmt.Errorf("listing images: %w", err)
355}
356
357digest := descriptor.Digest.String()
358for _, img := range images {
359imgFullName := fmt.Sprintf("%s:%s", img.Repository, img.Tag)
360if 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.
370return ociStore.Untag(ctx, fullName)
371}
372}
373
374err = ociStore.Delete(ctx, descriptor)
375if err != nil {
376return err
377}
378
379return 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
388func splitIGDomain(name string) (domain, remainder string) {
389i := strings.IndexRune(name, '/')
390if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != localhost && strings.ToLower(name[:i]) == name[:i]) {
391domain, remainder = defaultDomain, name
392} else {
393domain, remainder = name[:i], name[i+1:]
394}
395if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
396remainder = officialRepoPrefix + remainder
397}
398return
399}
400
401func normalizeImageName(image string) (reference.Named, error) {
402// Use the default gadget's registry if no domain is specified.
403domain, remainer := splitIGDomain(image)
404
405name, err := reference.ParseNormalizedNamed(domain + "/" + remainer)
406if err != nil {
407return nil, fmt.Errorf("parsing normalized image %q: %w", image, err)
408}
409return reference.TagNameOnly(name), nil
410}
411
412func getHostString(repository string) (string, error) {
413repo, err := reference.Parse(repository)
414if err != nil {
415return "", fmt.Errorf("parsing repository %q: %w", repository, err)
416}
417if named, ok := repo.(reference.Named); ok {
418return reference.Domain(named), nil
419}
420return "", fmt.Errorf("image has to be a named reference")
421}
422
423func newAuthClient(repository string, authOptions *AuthOptions) (*oras_auth.Client, error) {
424log.Debugf("Using auth file %q", authOptions.AuthFile)
425
426var cfg *configfile.ConfigFile
427var err error
428
429if authOptions.SecretBytes != nil && len(authOptions.SecretBytes) != 0 {
430cfg, err = config.LoadFromReader(bytes.NewReader(authOptions.SecretBytes))
431if err != nil {
432return 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
437if !errors.Is(err, os.ErrNotExist) || authOptions.AuthFile != DefaultAuthFile {
438return nil, fmt.Errorf("opening auth file %q: %w", authOptions.AuthFile, err)
439}
440
441log.Debugf("Couldn't find default auth file %q...", authOptions.AuthFile)
442log.Debugf("Using default docker auth file instead")
443log.Debugf("$HOME: %q", os.Getenv("HOME"))
444
445cfg, err = config.Load("")
446if err != nil {
447return nil, fmt.Errorf("loading auth config: %w", err)
448}
449
450} else {
451defer authFileReader.Close()
452cfg, err = config.LoadFromReader(authFileReader)
453if err != nil {
454return nil, fmt.Errorf("loading auth config: %w", err)
455}
456}
457
458hostString, err := getHostString(repository)
459if err != nil {
460return nil, fmt.Errorf("getting host string: %w", err)
461}
462authConfig, err := cfg.GetAuthConfig(hostString)
463if err != nil {
464return nil, fmt.Errorf("getting auth config: %w", err)
465}
466
467return &oras_auth.Client{
468Credential: oras_auth.StaticCredential(hostString, oras_auth.Credential{
469Username: authConfig.Username,
470Password: authConfig.Password,
471AccessToken: authConfig.Auth,
472RefreshToken: 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.
479func newRepository(image reference.Named, authOpts *AuthOptions) (*remote.Repository, error) {
480repo, err := remote.NewRepository(image.Name())
481if err != nil {
482return nil, fmt.Errorf("creating remote repository: %w", err)
483}
484repo.PlainHTTP = authOpts.Insecure
485if !authOpts.Insecure {
486client, err := newAuthClient(image.Name(), authOpts)
487if err != nil {
488return nil, fmt.Errorf("creating auth client: %w", err)
489}
490repo.Client = client
491}
492
493return repo, nil
494}
495
496func getImageListDescriptor(ctx context.Context, imageStore oras.ReadOnlyTarget, reference string) (ocispec.Index, error) {
497imageListDescriptor, err := imageStore.Resolve(ctx, reference)
498if err != nil {
499return ocispec.Index{}, fmt.Errorf("resolving image %q: %w", reference, err)
500}
501if imageListDescriptor.MediaType != ocispec.MediaTypeImageIndex {
502return ocispec.Index{}, fmt.Errorf("image %q is not an image index", reference)
503}
504
505reader, err := imageStore.Fetch(ctx, imageListDescriptor)
506if err != nil {
507return ocispec.Index{}, fmt.Errorf("fetching image index: %w", err)
508}
509defer reader.Close()
510
511var index ocispec.Index
512if err = json.NewDecoder(reader).Decode(&index); err != nil {
513return ocispec.Index{}, fmt.Errorf("unmarshalling image index: %w", err)
514}
515return index, nil
516}
517
518func getArchManifest(imageStore oras.ReadOnlyTarget, index ocispec.Index) (*ocispec.Manifest, error) {
519var manifestDesc ocispec.Descriptor
520for _, indexManifest := range index.Manifests {
521// TODO: Check docker code
522if indexManifest.Platform.Architecture == runtime.GOARCH {
523manifestDesc = indexManifest
524break
525}
526}
527if manifestDesc.Digest == "" {
528return nil, fmt.Errorf("no manifest found for architecture %q", runtime.GOARCH)
529}
530
531reader, err := imageStore.Fetch(context.TODO(), manifestDesc)
532if err != nil {
533return nil, fmt.Errorf("fetching manifest: %w", err)
534}
535defer reader.Close()
536
537var manifest ocispec.Manifest
538if err = json.NewDecoder(reader).Decode(&manifest); err != nil {
539return nil, fmt.Errorf("unmarshalling manifest: %w", err)
540}
541return &manifest, nil
542}
543
544func getMetadataFromManifest(ctx context.Context, target oras.Target, manifest *ocispec.Manifest) ([]byte, error) {
545// metadata is optional
546if manifest.Config.Size == 0 {
547return nil, nil
548}
549
550metadata, err := getContentFromDescriptor(ctx, target, manifest.Config)
551if err != nil {
552return nil, fmt.Errorf("getting metadata from descriptor: %w", err)
553}
554
555return metadata, nil
556}
557
558func getLayerFromManifest(ctx context.Context, target oras.Target, manifest *ocispec.Manifest, mediaType string) ([]byte, error) {
559var layer ocispec.Descriptor
560layerCount := 0
561for _, l := range manifest.Layers {
562if l.MediaType == mediaType {
563layer = l
564layerCount++
565}
566}
567if layerCount == 0 {
568return nil, nil
569}
570if layerCount != 1 {
571return nil, fmt.Errorf("expected exactly one layer with media type %q, got %d", mediaType, layerCount)
572}
573layerBytes, err := getContentFromDescriptor(ctx, target, layer)
574if err != nil {
575return nil, fmt.Errorf("getting layer %q from descriptor: %w", mediaType, err)
576}
577if len(layerBytes) == 0 {
578return nil, errors.New("layer is empty")
579}
580return layerBytes, nil
581}
582
583func getContentFromDescriptor(ctx context.Context, imageStore oras.ReadOnlyTarget, desc ocispec.Descriptor) ([]byte, error) {
584reader, err := imageStore.Fetch(ctx, desc)
585if err != nil {
586return nil, fmt.Errorf("fetching descriptor: %w", err)
587}
588defer reader.Close()
589bytes, err := io.ReadAll(reader)
590if err != nil {
591return nil, fmt.Errorf("reading descriptor: %w", err)
592}
593return bytes, nil
594}
595
596func getImageManifest(ctx context.Context, target oras.Target, image string, authOpts *AuthOptions) (*ocispec.Manifest, error) {
597imageRef, err := normalizeImageName(image)
598if err != nil {
599return nil, fmt.Errorf("normalizing image: %w", err)
600}
601
602index, err := getImageListDescriptor(ctx, target, imageRef.String())
603if err != nil {
604return nil, fmt.Errorf("getting image list descriptor: %w", err)
605}
606
607manifestHost, err := getArchManifest(target, index)
608if err != nil {
609return nil, fmt.Errorf("getting arch manifest: %w", err)
610}
611return manifestHost, nil
612}
613