github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/oci/build.go (about)

     1  // Copyright 2023-2024 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  
    26  	"github.com/distribution/reference"
    27  	"github.com/opencontainers/image-spec/specs-go"
    28  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    29  	"gopkg.in/yaml.v2"
    30  	"oras.land/oras-go/v2"
    31  	"oras.land/oras-go/v2/content"
    32  	"oras.land/oras-go/v2/errdef"
    33  
    34  	metadatav1 "github.com/inspektor-gadget/inspektor-gadget/pkg/metadata/v1"
    35  )
    36  
    37  const (
    38  	ArchAmd64 = "amd64"
    39  	ArchArm64 = "arm64"
    40  	ArchWasm  = "wasm"
    41  )
    42  
    43  const (
    44  	eBPFObjectMediaType = "application/vnd.gadget.ebpf.program.v1+binary"
    45  	wasmObjectMediaType = "application/vnd.gadget.wasm.program.v1+binary"
    46  	btfgenMediaType     = "application/vnd.gadget.btfgen.v1+binary"
    47  	metadataMediaType   = "application/vnd.gadget.config.v1+yaml"
    48  )
    49  
    50  type ObjectPath struct {
    51  	// Path to the EBPF object
    52  	EBPF string
    53  	// Optional path to the Wasm file
    54  	Wasm string
    55  	// Optional path to tarball containing BTF files generated with btfgen
    56  	Btfgen string
    57  }
    58  
    59  type BuildGadgetImageOpts struct {
    60  	// Source path of the eBPF program. Currently it's not used for compilation purposes
    61  	EBPFSourcePath string
    62  	// List of eBPF objects to include in the image. The key is the architecture and the value
    63  	// are the paths to the objects.
    64  	ObjectPaths map[string]*ObjectPath
    65  	// Path to the metadata file.
    66  	MetadataPath string
    67  	// If true, the metadata is updated to follow changes in the eBPF objects.
    68  	UpdateMetadata bool
    69  	// If true, the metadata is validated before creating the image.
    70  	ValidateMetadata bool
    71  	// Date and time on which the image is built (date-time string as defined by RFC 3339).
    72  	CreatedDate string
    73  }
    74  
    75  // BuildGadgetImage creates an OCI image with the objects provided in opts. The image parameter in
    76  // the "name:tag" format is used to name and tag the created image. If it's empty the image is not
    77  // named.
    78  func BuildGadgetImage(ctx context.Context, opts *BuildGadgetImageOpts, image string) (*GadgetImageDesc, error) {
    79  	ociStore, err := getLocalOciStore()
    80  	if err != nil {
    81  		return nil, fmt.Errorf("getting oci store: %w", err)
    82  	}
    83  
    84  	if opts.UpdateMetadata {
    85  		if err := createOrUpdateMetadataFile(ctx, opts); err != nil {
    86  			return nil, fmt.Errorf("updating metadata file: %w", err)
    87  		}
    88  	}
    89  
    90  	if opts.ValidateMetadata {
    91  		if err := validateMetadataFile(ctx, opts); err != nil && !errors.Is(err, os.ErrNotExist) {
    92  			return nil, fmt.Errorf("validating metadata file: %w", err)
    93  		}
    94  	}
    95  
    96  	indexDesc, err := createImageIndex(ctx, ociStore, opts)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("creating image index: %w", err)
    99  	}
   100  
   101  	imageDesc := &GadgetImageDesc{
   102  		Digest: indexDesc.Digest.String(),
   103  	}
   104  
   105  	if image != "" {
   106  		targetImage, err := normalizeImageName(image)
   107  		if err != nil {
   108  			return nil, fmt.Errorf("normalizing image: %w", err)
   109  		}
   110  
   111  		err = ociStore.Tag(ctx, indexDesc, targetImage.String())
   112  		if err != nil {
   113  			return nil, fmt.Errorf("tagging manifest: %w", err)
   114  		}
   115  
   116  		imageDesc.Repository = targetImage.Name()
   117  		if ref, ok := targetImage.(reference.Tagged); ok {
   118  			imageDesc.Tag = ref.Tag()
   119  		}
   120  	}
   121  
   122  	if err := fixGeneratedFilesOwner(opts); err != nil {
   123  		return nil, fmt.Errorf("fixing generated files owner: %w", err)
   124  	}
   125  
   126  	return imageDesc, nil
   127  }
   128  
   129  func pushDescriptorIfNotExists(ctx context.Context, target oras.Target, desc ocispec.Descriptor, contentReader io.Reader) error {
   130  	err := target.Push(ctx, desc, contentReader)
   131  	if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
   132  		return fmt.Errorf("pushing descriptor: %w", err)
   133  	}
   134  	return nil
   135  }
   136  
   137  func createLayerDesc(ctx context.Context, target oras.Target, progFilePath, mediaType string) (ocispec.Descriptor, error) {
   138  	progBytes, err := os.ReadFile(progFilePath)
   139  	if err != nil {
   140  		return ocispec.Descriptor{}, fmt.Errorf("reading eBPF program file: %w", err)
   141  	}
   142  	progDesc := content.NewDescriptorFromBytes(mediaType, progBytes)
   143  
   144  	err = pushDescriptorIfNotExists(ctx, target, progDesc, bytes.NewReader(progBytes))
   145  	if err != nil {
   146  		return ocispec.Descriptor{}, fmt.Errorf("pushing %q layer: %w", mediaType, err)
   147  	}
   148  
   149  	return progDesc, nil
   150  }
   151  
   152  func annotationsFromMetadata(metadataBytes []byte) (map[string]string, error) {
   153  	metadata := &metadatav1.GadgetMetadata{}
   154  	if err := yaml.NewDecoder(bytes.NewReader(metadataBytes)).Decode(&metadata); err != nil {
   155  		return nil, fmt.Errorf("decoding metadata file: %w", err)
   156  	}
   157  
   158  	// Suggested annotations for the OCI image
   159  	// https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
   160  	annotations := map[string]string{
   161  		ocispec.AnnotationTitle:         metadata.Name,
   162  		ocispec.AnnotationDescription:   metadata.Description,
   163  		ocispec.AnnotationURL:           metadata.HomepageURL,
   164  		ocispec.AnnotationDocumentation: metadata.DocumentationURL,
   165  		ocispec.AnnotationSource:        metadata.SourceURL,
   166  	}
   167  
   168  	for k, v := range metadata.Annotations {
   169  		annotations[k] = v
   170  	}
   171  	return annotations, nil
   172  }
   173  
   174  func createMetadataDesc(ctx context.Context, target oras.Target, metadataFilePath string) (ocispec.Descriptor, error) {
   175  	metadataBytes, err := os.ReadFile(metadataFilePath)
   176  	if err != nil {
   177  		return ocispec.Descriptor{}, fmt.Errorf("reading metadata file: %w", err)
   178  	}
   179  	defDesc := content.NewDescriptorFromBytes(metadataMediaType, metadataBytes)
   180  	defDesc.Annotations, err = annotationsFromMetadata(metadataBytes)
   181  	if err != nil {
   182  		return ocispec.Descriptor{}, fmt.Errorf("reading annotations from metadata file: %w", err)
   183  	}
   184  
   185  	err = pushDescriptorIfNotExists(ctx, target, defDesc, bytes.NewReader(metadataBytes))
   186  	if err != nil {
   187  		return ocispec.Descriptor{}, fmt.Errorf("pushing metadata file: %w", err)
   188  	}
   189  	return defDesc, nil
   190  }
   191  
   192  func createEmptyDesc(ctx context.Context, target oras.Target) (ocispec.Descriptor, error) {
   193  	emptyDesc := ocispec.DescriptorEmptyJSON
   194  	err := pushDescriptorIfNotExists(ctx, target, emptyDesc, bytes.NewReader(emptyDesc.Data))
   195  	if err != nil {
   196  		return ocispec.Descriptor{}, fmt.Errorf("pushing empty descriptor: %w", err)
   197  	}
   198  	return emptyDesc, nil
   199  }
   200  
   201  func createManifestForTarget(ctx context.Context, target oras.Target, metadataFilePath, arch string, paths *ObjectPath, createdDate string) (ocispec.Descriptor, error) {
   202  	layerDescs := []ocispec.Descriptor{}
   203  
   204  	progDesc, err := createLayerDesc(ctx, target, paths.EBPF, eBPFObjectMediaType)
   205  	if err != nil {
   206  		return ocispec.Descriptor{}, fmt.Errorf("creating and pushing eBPF descriptor: %w", err)
   207  	}
   208  	layerDescs = append(layerDescs, progDesc)
   209  
   210  	if paths.Wasm != "" {
   211  		wasmDesc, err := createLayerDesc(ctx, target, paths.Wasm, wasmObjectMediaType)
   212  		if err != nil {
   213  			return ocispec.Descriptor{}, fmt.Errorf("creating and pushing wasm descriptor: %w", err)
   214  		}
   215  		layerDescs = append(layerDescs, wasmDesc)
   216  	}
   217  
   218  	if paths.Btfgen != "" {
   219  		btfDesc, err := createLayerDesc(ctx, target, paths.Btfgen, btfgenMediaType)
   220  		if err != nil {
   221  			return ocispec.Descriptor{}, fmt.Errorf("creating and pushing btfgen descriptor: %w", err)
   222  		}
   223  		layerDescs = append(layerDescs, btfDesc)
   224  	}
   225  
   226  	var defDesc ocispec.Descriptor
   227  	// artifactType must be only set when the config.mediaType is set to
   228  	// MediaTypeEmptyJSON. In our case, when the metadata file is not provided:
   229  	// https://github.com/opencontainers/image-spec/blob/f5f87016de46439ccf91b5381cf76faaae2bc28f/manifest.md?plain=1#L170
   230  	var artifactType string
   231  
   232  	if _, err := os.Stat(metadataFilePath); err == nil {
   233  		// Read the metadata file into a byte array
   234  		defDesc, err = createMetadataDesc(ctx, target, metadataFilePath)
   235  		if err != nil {
   236  			return ocispec.Descriptor{}, fmt.Errorf("creating metadata descriptor: %w", err)
   237  		}
   238  		defDesc.Annotations[ocispec.AnnotationCreated] = createdDate
   239  	} else {
   240  		// Create an empty descriptor
   241  		defDesc, err = createEmptyDesc(ctx, target)
   242  		if err != nil {
   243  			return ocispec.Descriptor{}, fmt.Errorf("creating empty descriptor: %w", err)
   244  		}
   245  		artifactType = eBPFObjectMediaType
   246  
   247  		// Even without metadata, we can still set some annotations
   248  		defDesc.Annotations = map[string]string{
   249  			ocispec.AnnotationCreated: createdDate,
   250  		}
   251  	}
   252  
   253  	// Create the manifest which combines everything and push it to the memory store
   254  	manifest := ocispec.Manifest{
   255  		Versioned: specs.Versioned{
   256  			SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
   257  		},
   258  		Config:       defDesc,
   259  		Layers:       layerDescs,
   260  		Annotations:  defDesc.Annotations,
   261  		ArtifactType: artifactType,
   262  	}
   263  
   264  	manifestJson, err := json.Marshal(manifest)
   265  	if err != nil {
   266  		return ocispec.Descriptor{}, fmt.Errorf("marshalling manifest: %w", err)
   267  	}
   268  	manifestDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifestJson)
   269  	manifestDesc.Platform = &ocispec.Platform{
   270  		Architecture: arch,
   271  		OS:           "linux",
   272  	}
   273  	manifestDesc.Annotations = manifest.Annotations
   274  
   275  	exists, err := target.Exists(ctx, manifestDesc)
   276  	if err != nil {
   277  		return ocispec.Descriptor{}, fmt.Errorf("checking if manifest exists: %w", err)
   278  	}
   279  	if exists {
   280  		return manifestDesc, nil
   281  	}
   282  	err = pushDescriptorIfNotExists(ctx, target, manifestDesc, bytes.NewReader(manifestJson))
   283  	if err != nil {
   284  		return ocispec.Descriptor{}, fmt.Errorf("pushing manifest: %w", err)
   285  	}
   286  
   287  	return manifestDesc, nil
   288  }
   289  
   290  func createImageIndex(ctx context.Context, target oras.Target, o *BuildGadgetImageOpts) (ocispec.Descriptor, error) {
   291  	// Read the eBPF program files and push them to the memory store
   292  	layers := []ocispec.Descriptor{}
   293  
   294  	for arch, paths := range o.ObjectPaths {
   295  		manifestDesc, err := createManifestForTarget(ctx, target, o.MetadataPath, arch, paths, o.CreatedDate)
   296  		if err != nil {
   297  			return ocispec.Descriptor{}, fmt.Errorf("creating %s manifest: %w", arch, err)
   298  		}
   299  		layers = append(layers, manifestDesc)
   300  	}
   301  
   302  	if len(layers) == 0 {
   303  		return ocispec.Descriptor{}, fmt.Errorf("no eBPF objects found")
   304  	}
   305  
   306  	// Create the index which combines the architectures and push it to the memory store
   307  	index := ocispec.Index{
   308  		Versioned: specs.Versioned{
   309  			SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
   310  		},
   311  		MediaType:   ocispec.MediaTypeImageIndex,
   312  		Manifests:   layers,
   313  		Annotations: layers[0].Annotations,
   314  	}
   315  	indexJson, err := json.Marshal(index)
   316  	if err != nil {
   317  		return ocispec.Descriptor{}, fmt.Errorf("marshalling manifest: %w", err)
   318  	}
   319  	indexDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexJson)
   320  	indexDesc.Annotations = index.Annotations
   321  
   322  	err = pushDescriptorIfNotExists(ctx, target, indexDesc, bytes.NewReader(indexJson))
   323  	if err != nil {
   324  		return ocispec.Descriptor{}, fmt.Errorf("pushing manifest index: %w", err)
   325  	}
   326  	return indexDesc, nil
   327  }