...

Source file src/edge-infra.dev/pkg/f8n/warehouse/oci/artifact.go

Documentation: edge-infra.dev/pkg/f8n/warehouse/oci

     1  // Package oci contains OCI functionality and types that are used to support
     2  // Warehouse specific implementations in other packages. It is used to enforce
     3  // Warehouse semantics on top of OCI semantics in a reusable way.
     4  //
     5  // Behavior that is specific to types of Warehouse packages (e.g., Pallets),
     6  // should be placed in that package, not here.
     7  package oci
     8  
     9  import (
    10  	"bytes"
    11  	"fmt"
    12  
    13  	v1 "github.com/google/go-containerregistry/pkg/v1"
    14  	"github.com/google/go-containerregistry/pkg/v1/mutate"
    15  	"github.com/google/go-containerregistry/pkg/v1/partial"
    16  
    17  	"edge-infra.dev/pkg/f8n/warehouse"
    18  )
    19  
    20  // Artifact represents the intersection between v1.Image and v1.ImageIndex by
    21  // embedding the two partial interfaces required for common OCI operations in
    22  // google/go-containerregistry. This interface allows us to pass around warehouse
    23  // artifacts without concerning callers about whether or not the artifact is an
    24  // v1.Image or v1.ImageIndex.
    25  //
    26  // Artifacts can be used to represent both Pallets and Cluster package types,
    27  // as well as any future Warehouse package types, as long as they embed this
    28  // interface.
    29  type Artifact interface {
    30  	// Describable represents anything that we can produce a descriptor for,
    31  	// e.g., what gets embedded in in v1.Image and v1.ImageIndex to reference
    32  	// other OCI entities.
    33  	// Specifically, that means things that implement partial.Describable can be
    34  	// appended or composed with existing artifacts via the mutate package.
    35  	partial.Describable
    36  
    37  	// WithRawManifest is the lowest common denominator which can be used to
    38  	// implement v1.Image and v1.ImageIndex. Nearly all implementations for those
    39  	// interfaces in google/go-containerregistry embed this interface, allowing
    40  	// us to easily use those packages when working with artifacts.
    41  	partial.WithRawManifest
    42  }
    43  
    44  // Unwrapper allows access to the underlying OCI artifact via the Unwrap method.
    45  type Unwrapper interface {
    46  	// Unwrap returns the underlying oci.Artifact, allowing for matching based on
    47  	// ggcr interface types (v1.ImageIndex, v1.Image, etc)
    48  	Unwrap() Artifact
    49  }
    50  
    51  // Annotate adds a series of annotation maps to an OCI artifact and returns
    52  // the annotated artifact.
    53  // Note that OCI artifacts are immutable: this function is really creating a new
    54  // artifact that is the input + the annotations.
    55  //
    56  // Does not do key collision checks.  Must be casted back to original type (add example)
    57  func Annotate(a partial.WithRawManifest, annos ...map[string]string) partial.WithRawManifest {
    58  	m := map[string]string{}
    59  	for _, anno := range annos {
    60  		for k, v := range anno {
    61  			m[k] = v
    62  		}
    63  	}
    64  
    65  	return mutate.Annotations(a, m)
    66  }
    67  
    68  // Annotations returns the OCI manifest annotations for the input artifact after
    69  // inferring the concrete type.
    70  func Annotations(a Artifact) (map[string]string, error) {
    71  	switch a := a.(type) {
    72  	case Unwrapper:
    73  		return Annotations(a.Unwrap())
    74  	case v1.Image:
    75  		// lazily evaluate manifest to avoid doing it twice if input is Unwrapper
    76  		raw, err := a.RawManifest()
    77  		if err != nil {
    78  			return nil, fmt.Errorf("failed to load oci manifest from artifact: %w", err)
    79  		}
    80  		manifest, err := v1.ParseManifest(bytes.NewReader(raw))
    81  		if err != nil {
    82  			return nil, fmt.Errorf("failed to parse image manifest: %w", err)
    83  		}
    84  		return manifest.Annotations, nil
    85  	case v1.ImageIndex:
    86  		// lazily evaluate manifest to avoid doing it twice if input is Unwrapper
    87  		raw, err := a.RawManifest()
    88  		if err != nil {
    89  			return nil, fmt.Errorf("failed to load oci manifest from artifact: %w", err)
    90  		}
    91  		manifest, err := v1.ParseIndexManifest(bytes.NewReader(raw))
    92  		if err != nil {
    93  			return nil, fmt.Errorf("failed to parse image index manifest: %w", err)
    94  		}
    95  		return manifest.Annotations, nil
    96  	default:
    97  		return nil, fmt.Errorf("%w", ErrInvalidArtifact)
    98  	}
    99  }
   100  
   101  // Contains returns true if the v1.ImageIndex embeds a descriptor referencing the
   102  // Artifact, after inferring the concrete type. The descriptor's ref name
   103  // annotation must match the Artifacts name annotation and the digests must match.
   104  func Contains(idx v1.ImageIndex, a Artifact) (bool, error) {
   105  	digest, err := a.Digest()
   106  	if err != nil {
   107  		return false, err
   108  	}
   109  	annos, err := Annotations(a)
   110  	if err != nil {
   111  		return false, err
   112  	}
   113  
   114  	idxm, err := idx.IndexManifest()
   115  	if err != nil {
   116  		return false, err
   117  	}
   118  
   119  	for _, m := range idxm.Manifests {
   120  		if m.Annotations[warehouse.AnnotationRefName] ==
   121  			annos[warehouse.AnnotationName] && digest == m.Digest {
   122  			return true, nil
   123  		}
   124  	}
   125  
   126  	return false, nil
   127  }
   128  
   129  // IsPackage determines if the v1.Image is a standalone package or not by checking
   130  // if it is embedded into the Artifact and has the same name as the
   131  // Artifact. e.g., v1.Images named shoot will be embedded into a v1.ImageIndex
   132  // named shoot in order to represent different provider variants, or if shoot has
   133  // dependencies.
   134  //
   135  // If the Artifact is nil, the Image is assumed to be a package. Anything other
   136  // than nil or an ImageIndex will return an error, as it means you have goofed
   137  // up pretty thoroughly.
   138  //
   139  // If an ImageIndex contains an Image, that represents either an ImageIndex for
   140  // a package with provider-specific variants (shoot:gke, shoot:sds) or a package
   141  // with dependencies. This is a useful signal while walking artifacts from root
   142  // nodes. If you only care about walking the root nodes of each individual package
   143  // in the graph, you can use this function to skip walking Images
   144  func IsPackage(img v1.Image, parent Artifact) (bool, error) {
   145  	imgAnnos, err := Annotations(img)
   146  	if err != nil {
   147  		return false, err
   148  	}
   149  
   150  	switch a := parent.(type) {
   151  	case Unwrapper:
   152  		return IsPackage(img, a.Unwrap())
   153  	case v1.ImageIndex:
   154  		manifest, err := a.IndexManifest()
   155  		if err != nil {
   156  			return false, err
   157  		}
   158  
   159  		return imgAnnos[warehouse.AnnotationName] !=
   160  			manifest.Annotations[warehouse.AnnotationName], nil
   161  
   162  		// confirm that idx embeds image explicitly
   163  		// this level of safety isn't explicitly required in the context of
   164  		// using oci.Walk, since oci.Walk is traversing the descriptors it is
   165  		// guaranteed that the parent embeds the input img. however, that
   166  		// isn't the case if some other client was using IsPackage() outside of
   167  		// the conext of walk. if that use case ever shows up, uncomment this
   168  		// code:
   169  		// refs, err := Contains(a, img)
   170  		// if err != nil {
   171  		// 	return false, err
   172  		// }
   173  		// return !refs, nil
   174  	case nil:
   175  		return true, nil
   176  	default:
   177  		return false, fmt.Errorf("non-ImageIndex type %T was provided as parent", a)
   178  	}
   179  }
   180  
   181  // IsComposite returns true if the Artifact is a v1.ImageIndex which only contains
   182  // embedded descriptors referencing other v1.ImageIndexes or standalone packages
   183  // that are v1.Images. This means that the index is a composite index that only
   184  // exists to compose multiple distinct package together.
   185  //
   186  // If the Artifact is anything but an ImageIndex, it exits gracefully with false.
   187  func IsComposite(a Artifact) (bool, error) {
   188  	switch a := a.(type) {
   189  	case Unwrapper:
   190  		return IsComposite(a.Unwrap())
   191  	case v1.ImageIndex:
   192  		manifest, err := a.IndexManifest()
   193  		if err != nil {
   194  			return false, err
   195  		}
   196  		for _, m := range manifest.Manifests {
   197  			if !m.MediaType.IsIndex() &&
   198  				m.Annotations[warehouse.AnnotationRefName] ==
   199  					manifest.Annotations[warehouse.AnnotationName] {
   200  				return false, nil
   201  			}
   202  		}
   203  		return true, nil
   204  	default:
   205  		return false, nil
   206  	}
   207  }
   208  
   209  // TODO: keep? doc?
   210  func ArtifactFromIdx(idx v1.ImageIndex, d v1.Descriptor) (Artifact, error) {
   211  	switch {
   212  	case d.MediaType.IsImage():
   213  		return idx.Image(d.Digest)
   214  	case d.MediaType.IsIndex():
   215  		return idx.ImageIndex(d.Digest)
   216  	default:
   217  		return nil, fmt.Errorf("%w: unexpected mediaType %s",
   218  			ErrInvalidArtifact, d.MediaType)
   219  	}
   220  }
   221  

View as plain text