github.com/docker/cnab-to-oci@v0.3.0-beta4/converter/convert.go (about)

     1  package converter
     2  
     3  import (
     4  	_ "crypto/sha256" // this ensures we can parse sha256 digests
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"sort"
     9  
    10  	"github.com/cnabio/cnab-go/bundle"
    11  	"github.com/containerd/containerd/images"
    12  	"github.com/docker/cnab-to-oci/relocation"
    13  	"github.com/docker/distribution/reference"
    14  	ocischema "github.com/opencontainers/image-spec/specs-go"
    15  	ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1"
    16  )
    17  
    18  const ( // General values
    19  	// CNABVersion is the currently supported CNAB runtime version
    20  	CNABVersion = "v1.0.0"
    21  
    22  	// OCIIndexSchemaVersion is the currently supported OCI index schema's version
    23  	OCIIndexSchemaVersion = 2
    24  )
    25  
    26  type cnabDescriptorTypeValue = string
    27  
    28  const ( // Top Level annotations and values
    29  	// CNABRuntimeVersionAnnotation is the top level annotation specifying the CNAB runtime version
    30  	CNABRuntimeVersionAnnotation = "io.cnab.runtime_version"
    31  	// CNABKeywordsAnnotation is the top level annotation specifying a list of keywords
    32  	CNABKeywordsAnnotation = "io.cnab.keywords"
    33  	// ArtifactTypeAnnotation is the top level annotation specifying the type of the artifact in the registry
    34  	ArtifactTypeAnnotation = "org.opencontainers.artifactType"
    35  	// ArtifactTypeValue is the value of ArtifactTypeAnnotion for CNAB bundles
    36  	ArtifactTypeValue = "application/vnd.cnab.manifest.v1"
    37  )
    38  
    39  const ( // Descriptor level annotations and values
    40  	// CNABDescriptorTypeAnnotation is a descriptor-level annotation specifying the type of reference image (currently invocation or component)
    41  	CNABDescriptorTypeAnnotation = "io.cnab.manifest.type"
    42  	// CNABDescriptorTypeInvocation is the CNABDescriptorTypeAnnotation value for invocation images
    43  	CNABDescriptorTypeInvocation cnabDescriptorTypeValue = "invocation"
    44  	// CNABDescriptorTypeComponent is the CNABDescriptorTypeAnnotation value for component images
    45  	CNABDescriptorTypeComponent cnabDescriptorTypeValue = "component"
    46  	// CNABDescriptorTypeConfig is the CNABDescriptorTypeAnnotation value for bundle configuration
    47  	CNABDescriptorTypeConfig cnabDescriptorTypeValue = "config"
    48  
    49  	// CNABDescriptorComponentNameAnnotation is a decriptor-level annotation specifying the component name
    50  	CNABDescriptorComponentNameAnnotation = "io.cnab.component.name"
    51  )
    52  
    53  // GetBundleConfigManifestDescriptor returns the CNAB runtime config manifest descriptor from a OCI index
    54  func GetBundleConfigManifestDescriptor(ix *ocischemav1.Index) (ocischemav1.Descriptor, error) {
    55  	for _, d := range ix.Manifests {
    56  		if d.Annotations[CNABDescriptorTypeAnnotation] == CNABDescriptorTypeConfig {
    57  			return d, nil
    58  		}
    59  	}
    60  	return ocischemav1.Descriptor{}, errors.New("bundle config not found")
    61  }
    62  
    63  // ConvertBundleToOCIIndex converts a CNAB bundle into an OCI Index representation
    64  func ConvertBundleToOCIIndex(b *bundle.Bundle, targetRef reference.Named,
    65  	bundleConfigManifestRef ocischemav1.Descriptor, relocationMap relocation.ImageRelocationMap) (*ocischemav1.Index, error) {
    66  	annotations, err := makeAnnotations(b)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	manifests, err := makeManifests(b, targetRef, bundleConfigManifestRef, relocationMap)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	result := ocischemav1.Index{
    75  		Versioned: ocischema.Versioned{
    76  			SchemaVersion: OCIIndexSchemaVersion,
    77  		},
    78  		Annotations: annotations,
    79  		Manifests:   manifests,
    80  	}
    81  	return &result, nil
    82  }
    83  
    84  // GenerateRelocationMap generates the bundle relocation map
    85  func GenerateRelocationMap(ix *ocischemav1.Index, b *bundle.Bundle, originRepo reference.Named) (relocation.ImageRelocationMap, error) {
    86  	relocationMap := relocation.ImageRelocationMap{}
    87  
    88  	for _, d := range ix.Manifests {
    89  		switch d.MediaType {
    90  		case ocischemav1.MediaTypeImageManifest, ocischemav1.MediaTypeImageIndex:
    91  		case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList:
    92  		default:
    93  			return nil, fmt.Errorf("unsupported manifest descriptor %q with mediatype %q", d.Digest, d.MediaType)
    94  		}
    95  		descriptorType, ok := d.Annotations[CNABDescriptorTypeAnnotation]
    96  		if !ok {
    97  			return nil, fmt.Errorf("manifest descriptor %q has no CNAB descriptor type annotation %q", d.Digest, CNABDescriptorTypeAnnotation)
    98  		}
    99  		if descriptorType == CNABDescriptorTypeConfig {
   100  			continue
   101  		}
   102  		// strip tag/digest from originRepo
   103  		originRepo, err := reference.ParseNormalizedNamed(originRepo.Name())
   104  		if err != nil {
   105  			return nil, fmt.Errorf("failed to create a digested reference for manifest descriptor %q: %s", d.Digest, err)
   106  		}
   107  		ref, err := reference.WithDigest(originRepo, d.Digest)
   108  		if err != nil {
   109  			return nil, fmt.Errorf("failed to create a digested reference for manifest descriptor %q: %s", d.Digest, err)
   110  		}
   111  		refFamiliar := reference.FamiliarString(ref)
   112  		switch descriptorType {
   113  		// The current descriptor is an invocation image
   114  		case CNABDescriptorTypeInvocation:
   115  			if len(b.InvocationImages) == 0 {
   116  				return nil, fmt.Errorf("unknown invocation image: %q", d.Digest)
   117  			}
   118  			relocationMap[b.InvocationImages[0].Image] = refFamiliar
   119  
   120  		// The current descriptor is a component image
   121  		case CNABDescriptorTypeComponent:
   122  			componentName, ok := d.Annotations[CNABDescriptorComponentNameAnnotation]
   123  			if !ok {
   124  				return nil, fmt.Errorf("component name missing in descriptor %q", d.Digest)
   125  			}
   126  			c, ok := b.Images[componentName]
   127  			if !ok {
   128  				return nil, fmt.Errorf("component %q not found in bundle", componentName)
   129  			}
   130  			relocationMap[c.Image] = refFamiliar
   131  		default:
   132  			return nil, fmt.Errorf("invalid CNAB descriptor type %q in descriptor %q", descriptorType, d.Digest)
   133  		}
   134  	}
   135  
   136  	return relocationMap, nil
   137  }
   138  
   139  func makeAnnotations(b *bundle.Bundle) (map[string]string, error) {
   140  	result := map[string]string{
   141  		CNABRuntimeVersionAnnotation:      b.SchemaVersion,
   142  		ocischemav1.AnnotationTitle:       b.Name,
   143  		ocischemav1.AnnotationVersion:     b.Version,
   144  		ocischemav1.AnnotationDescription: b.Description,
   145  		ArtifactTypeAnnotation:            ArtifactTypeValue,
   146  	}
   147  	if b.Maintainers != nil {
   148  		maintainers, err := json.Marshal(b.Maintainers)
   149  		if err != nil {
   150  			return nil, err
   151  		}
   152  		result[ocischemav1.AnnotationAuthors] = string(maintainers)
   153  	}
   154  	if b.Keywords != nil {
   155  		keywords, err := json.Marshal(b.Keywords)
   156  		if err != nil {
   157  			return nil, err
   158  		}
   159  		result[CNABKeywordsAnnotation] = string(keywords)
   160  	}
   161  	return result, nil
   162  }
   163  
   164  func makeManifests(b *bundle.Bundle, targetReference reference.Named,
   165  	bundleConfigManifestReference ocischemav1.Descriptor, relocationMap relocation.ImageRelocationMap) ([]ocischemav1.Descriptor, error) {
   166  	if len(b.InvocationImages) != 1 {
   167  		return nil, errors.New("only one invocation image supported")
   168  	}
   169  	if bundleConfigManifestReference.Annotations == nil {
   170  		bundleConfigManifestReference.Annotations = map[string]string{}
   171  	}
   172  	bundleConfigManifestReference.Annotations[CNABDescriptorTypeAnnotation] = CNABDescriptorTypeConfig
   173  	manifests := []ocischemav1.Descriptor{bundleConfigManifestReference}
   174  	invocationImage, err := makeDescriptor(b.InvocationImages[0].BaseImage, targetReference, relocationMap)
   175  	if err != nil {
   176  		return nil, fmt.Errorf("invalid invocation image: %s", err)
   177  	}
   178  	invocationImage.Annotations = map[string]string{
   179  		CNABDescriptorTypeAnnotation: CNABDescriptorTypeInvocation,
   180  	}
   181  	manifests = append(manifests, invocationImage)
   182  	images := makeSortedImages(b.Images)
   183  	for _, name := range images {
   184  		img := b.Images[name]
   185  		image, err := makeDescriptor(img.BaseImage, targetReference, relocationMap)
   186  		if err != nil {
   187  			return nil, fmt.Errorf("invalid image: %s", err)
   188  		}
   189  		image.Annotations = map[string]string{
   190  			CNABDescriptorTypeAnnotation:          CNABDescriptorTypeComponent,
   191  			CNABDescriptorComponentNameAnnotation: name,
   192  		}
   193  		manifests = append(manifests, image)
   194  	}
   195  	return manifests, nil
   196  }
   197  
   198  func makeSortedImages(images map[string]bundle.Image) []string {
   199  	var result []string
   200  	for k := range images {
   201  		result = append(result, k)
   202  	}
   203  	sort.Strings(result)
   204  	return result
   205  }
   206  
   207  func makeDescriptor(baseImage bundle.BaseImage, targetReference reference.Named, relocationMap relocation.ImageRelocationMap) (ocischemav1.Descriptor, error) {
   208  	relocatedImage, ok := relocationMap[baseImage.Image]
   209  	if !ok {
   210  		return ocischemav1.Descriptor{}, fmt.Errorf("image %q not present in the relocation map", baseImage.Image)
   211  	}
   212  
   213  	named, err := reference.ParseNormalizedNamed(relocatedImage)
   214  	if err != nil {
   215  		return ocischemav1.Descriptor{}, fmt.Errorf("image %q is not a valid image reference: %s", relocatedImage, err)
   216  	}
   217  	if named.Name() != targetReference.Name() {
   218  		return ocischemav1.Descriptor{}, fmt.Errorf("image %q is not in the same repository as %q", relocatedImage, targetReference.String())
   219  	}
   220  	digested, ok := named.(reference.Digested)
   221  	if !ok {
   222  		return ocischemav1.Descriptor{}, fmt.Errorf("image %q is not a digested reference", relocatedImage)
   223  	}
   224  	mediaType, err := getMediaType(baseImage, relocatedImage)
   225  	if err != nil {
   226  		return ocischemav1.Descriptor{}, err
   227  	}
   228  	if baseImage.Size == 0 {
   229  		return ocischemav1.Descriptor{}, fmt.Errorf("image %q size is not set", relocatedImage)
   230  	}
   231  
   232  	return ocischemav1.Descriptor{
   233  		Digest:    digested.Digest(),
   234  		MediaType: mediaType,
   235  		Size:      int64(baseImage.Size),
   236  	}, nil
   237  }
   238  
   239  func getMediaType(baseImage bundle.BaseImage, relocatedImage string) (string, error) {
   240  	mediaType := baseImage.MediaType
   241  	if mediaType == "" {
   242  		switch baseImage.ImageType {
   243  		case "docker":
   244  			mediaType = images.MediaTypeDockerSchema2Manifest
   245  		case "oci":
   246  			mediaType = ocischemav1.MediaTypeImageManifest
   247  		default:
   248  			return "", fmt.Errorf("unsupported image type %q for image %q", baseImage.ImageType, relocatedImage)
   249  		}
   250  	}
   251  	switch mediaType {
   252  	case ocischemav1.MediaTypeImageManifest:
   253  	case images.MediaTypeDockerSchema2Manifest:
   254  	case ocischemav1.MediaTypeImageIndex:
   255  	case images.MediaTypeDockerSchema2ManifestList:
   256  	default:
   257  		return "", fmt.Errorf("unsupported media type %q for image %q", baseImage.MediaType, relocatedImage)
   258  	}
   259  	return mediaType, nil
   260  }