github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/pack.go (about) 1 /* 2 Copyright The ORAS Authors. 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 16 package oras 17 18 import ( 19 "bytes" 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "time" 25 26 "github.com/opcr-io/oras-go/v2/content" 27 "github.com/opcr-io/oras-go/v2/errdef" 28 specs "github.com/opencontainers/image-spec/specs-go" 29 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 30 ) 31 32 const ( 33 // MediaTypeUnknownConfig is the default mediaType used when no 34 // config media type is specified. 35 MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json" 36 // MediaTypeUnknownArtifact is the default artifactType used when no 37 // artifact type is specified. 38 MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1" 39 ) 40 41 // ErrInvalidDateTimeFormat is returned by Pack() when 42 // AnnotationArtifactCreated or AnnotationCreated is provided, but its value 43 // is not in RFC 3339 format. 44 // Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 45 var ErrInvalidDateTimeFormat = errors.New("invalid date and time format") 46 47 // PackOptions contains parameters for [oras.Pack]. 48 type PackOptions struct { 49 // Subject is the subject of the manifest. 50 Subject *ocispec.Descriptor 51 // ManifestAnnotations is the annotation map of the manifest. 52 ManifestAnnotations map[string]string 53 54 // PackImageManifest controls whether to pack an image manifest or not. 55 // - If true, pack an image manifest; artifactType will be used as the 56 // the config descriptor mediaType of the image manifest. 57 // - If false, pack an artifact manifest. 58 // Default: false. 59 PackImageManifest bool 60 // ConfigDescriptor is a pointer to the descriptor of the config blob. 61 // If not nil, artifactType will be implied by the mediaType of the 62 // specified ConfigDescriptor, and ConfigAnnotations will be ignored. 63 // This option is valid only when PackImageManifest is true. 64 ConfigDescriptor *ocispec.Descriptor 65 // ConfigAnnotations is the annotation map of the config descriptor. 66 // This option is valid only when PackImageManifest is true 67 // and ConfigDescriptor is nil. 68 ConfigAnnotations map[string]string 69 } 70 71 // Pack packs the given blobs, generates a manifest for the pack, 72 // and pushes it to a content storage. 73 // 74 // When opts.PackImageManifest is true, artifactType will be used as the 75 // the config descriptor mediaType of the image manifest. 76 // If succeeded, returns a descriptor of the manifest. 77 func Pack(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { 78 if opts.PackImageManifest { 79 return packImage(ctx, pusher, artifactType, blobs, opts) 80 } 81 return packArtifact(ctx, pusher, artifactType, blobs, opts) 82 } 83 84 // packArtifact packs the given blobs, generates an artifact manifest for the 85 // pack, and pushes it to a content storage. 86 // If succeeded, returns a descriptor of the manifest. 87 func packArtifact(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { 88 if artifactType == "" { 89 artifactType = MediaTypeUnknownArtifact 90 } 91 92 annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) 93 if err != nil { 94 return ocispec.Descriptor{}, err 95 } 96 manifest := ocispec.Manifest{ 97 MediaType: ocispec.MediaTypeImageManifest, 98 ArtifactType: artifactType, 99 Subject: opts.Subject, 100 Annotations: annotations, 101 } 102 manifestJSON, err := json.Marshal(manifest) 103 if err != nil { 104 return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err) 105 } 106 manifestDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifestJSON) 107 // populate ArtifactType and Annotations of the manifest into manifestDesc 108 manifestDesc.ArtifactType = manifest.ArtifactType 109 manifestDesc.Annotations = manifest.Annotations 110 111 // push manifest 112 if err := pusher.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { 113 return ocispec.Descriptor{}, fmt.Errorf("failed to push manifest: %w", err) 114 } 115 116 return manifestDesc, nil 117 } 118 119 // packImage packs the given blobs, generates an image manifest for the pack, 120 // and pushes it to a content storage. artifactType will be used as the config 121 // descriptor mediaType of the image manifest. 122 // If succeeded, returns a descriptor of the manifest. 123 func packImage(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { 124 if configMediaType == "" { 125 configMediaType = MediaTypeUnknownConfig 126 } 127 128 var configDesc ocispec.Descriptor 129 if opts.ConfigDescriptor != nil { 130 configDesc = *opts.ConfigDescriptor 131 } else { 132 // Use an empty JSON object here, because some registries may not accept 133 // empty config blob. 134 // As of September 2022, GAR is known to return 400 on empty blob upload. 135 // See https://github.com/oras-project/oras-go/issues/294 for details. 136 configBytes := []byte("{}") 137 configDesc = content.NewDescriptorFromBytes(configMediaType, configBytes) 138 configDesc.Annotations = opts.ConfigAnnotations 139 // push config 140 if err := pusher.Push(ctx, configDesc, bytes.NewReader(configBytes)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { 141 return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) 142 } 143 } 144 145 annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) 146 if err != nil { 147 return ocispec.Descriptor{}, err 148 } 149 if layers == nil { 150 layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs 151 } 152 manifest := ocispec.Manifest{ 153 Versioned: specs.Versioned{ 154 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 155 }, 156 Config: configDesc, 157 MediaType: ocispec.MediaTypeImageManifest, 158 Layers: layers, 159 Subject: opts.Subject, 160 Annotations: annotations, 161 } 162 manifestJSON, err := json.Marshal(manifest) 163 if err != nil { 164 return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err) 165 } 166 manifestDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifestJSON) 167 // populate ArtifactType and Annotations of the manifest into manifestDesc 168 manifestDesc.ArtifactType = manifest.Config.MediaType 169 manifestDesc.Annotations = manifest.Annotations 170 171 // push manifest 172 if err := pusher.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { 173 return ocispec.Descriptor{}, fmt.Errorf("failed to push manifest: %w", err) 174 } 175 176 return manifestDesc, nil 177 } 178 179 // ensureAnnotationCreated ensures that annotationCreatedKey is in annotations, 180 // and that its value conforms to RFC 3339. Otherwise returns a new annotation 181 // map with annotationCreatedKey created. 182 func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey string) (map[string]string, error) { 183 if createdTime, ok := annotations[annotationCreatedKey]; ok { 184 // if annotationCreatedKey is provided, validate its format 185 if _, err := time.Parse(time.RFC3339, createdTime); err != nil { 186 return nil, fmt.Errorf("%w: %v", ErrInvalidDateTimeFormat, err) 187 } 188 return annotations, nil 189 } 190 191 // copy the original annotation map 192 copied := make(map[string]string, len(annotations)+1) 193 for k, v := range annotations { 194 copied[k] = v 195 } 196 // set creation time in RFC 3339 format 197 // reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/annotations.md#pre-defined-annotation-keys 198 now := time.Now().UTC() 199 copied[annotationCreatedKey] = now.Format(time.RFC3339) 200 return copied, nil 201 }