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 }