oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/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 "maps" 25 "regexp" 26 "time" 27 28 specs "github.com/opencontainers/image-spec/specs-go" 29 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 30 "oras.land/oras-go/v2/content" 31 "oras.land/oras-go/v2/errdef" 32 "oras.land/oras-go/v2/internal/spec" 33 ) 34 35 const ( 36 // MediaTypeUnknownConfig is the default config mediaType used 37 // - for [Pack] when PackOptions.PackImageManifest is true and 38 // PackOptions.ConfigDescriptor is not specified. 39 // - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 40 // and PackManifestOptions.ConfigDescriptor is not specified. 41 MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json" 42 43 // MediaTypeUnknownArtifact is the default artifactType used for [Pack] 44 // when PackOptions.PackImageManifest is false and artifactType is 45 // not specified. 46 MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1" 47 ) 48 49 var ( 50 // ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when 51 // "org.opencontainers.artifact.created" or "org.opencontainers.image.created" 52 // is provided, but its value is not in RFC 3339 format. 53 // Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 54 ErrInvalidDateTimeFormat = errors.New("invalid date and time format") 55 56 // ErrMissingArtifactType is returned by [PackManifest] when 57 // packManifestVersion is PackManifestVersion1_1 and artifactType is 58 // empty and the config media type is set to 59 // "application/vnd.oci.empty.v1+json". 60 ErrMissingArtifactType = errors.New("missing artifact type") 61 ) 62 63 // PackManifestVersion represents the manifest version used for [PackManifest]. 64 type PackManifestVersion int 65 66 const ( 67 // PackManifestVersion1_0 represents the OCI Image Manifest defined in 68 // image-spec v1.0.2. 69 // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md 70 PackManifestVersion1_0 PackManifestVersion = 1 71 72 // PackManifestVersion1_1_RC4 represents the OCI Image Manifest defined 73 // in image-spec v1.1.0-rc4. 74 // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md 75 // 76 // Deprecated: This constant is deprecated and not recommended for future use. 77 // Use [PackManifestVersion1_1] instead. 78 PackManifestVersion1_1_RC4 PackManifestVersion = PackManifestVersion1_1 79 80 // PackManifestVersion1_1 represents the OCI Image Manifest defined in 81 // image-spec v1.1.0. 82 // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md 83 PackManifestVersion1_1 PackManifestVersion = 2 84 ) 85 86 // PackManifestOptions contains optional parameters for [PackManifest]. 87 type PackManifestOptions struct { 88 // Subject is the subject of the manifest. 89 // This option is only valid when PackManifestVersion is 90 // NOT PackManifestVersion1_0. 91 Subject *ocispec.Descriptor 92 93 // Layers is the layers of the manifest. 94 Layers []ocispec.Descriptor 95 96 // ManifestAnnotations is the annotation map of the manifest. In order to 97 // make [PackManifest] reproducible, set the key ocispec.AnnotationCreated 98 // (i.e. "org.opencontainers.image.created") to a fixed value. The value 99 // must conform to RFC 3339. 100 ManifestAnnotations map[string]string 101 102 // ConfigDescriptor is a pointer to the descriptor of the config blob. 103 // If not nil, ConfigAnnotations will be ignored. 104 ConfigDescriptor *ocispec.Descriptor 105 106 // ConfigAnnotations is the annotation map of the config descriptor. 107 // This option is valid only when ConfigDescriptor is nil. 108 ConfigAnnotations map[string]string 109 } 110 111 // mediaTypeRegexp checks the format of media types. 112 // References: 113 // - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 114 // - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 115 var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$`) 116 117 // PackManifest generates an OCI Image Manifest based on the given parameters 118 // and pushes the packed manifest to a content storage using pusher. The version 119 // of the manifest to be packed is determined by packManifestVersion 120 // (Recommended value: PackManifestVersion1_1). 121 // 122 // - If packManifestVersion is [PackManifestVersion1_1]: 123 // artifactType MUST NOT be empty unless opts.ConfigDescriptor is specified. 124 // - If packManifestVersion is [PackManifestVersion1_0]: 125 // if opts.ConfigDescriptor is nil, artifactType will be used as the 126 // config media type; if artifactType is empty, 127 // "application/vnd.unknown.config.v1+json" will be used. 128 // if opts.ConfigDescriptor is NOT nil, artifactType will be ignored. 129 // 130 // artifactType and opts.ConfigDescriptor.MediaType MUST comply with RFC 6838. 131 // 132 // Each time when PackManifest is called, if a time stamp is not specified, a new time 133 // stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated 134 // (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible, 135 // set the key ocispec.AnnotationCreated to a fixed value in 136 // opts.ManifestAnnotations. The value MUST conform to RFC 3339. 137 // 138 // If succeeded, returns a descriptor of the packed manifest. 139 func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion PackManifestVersion, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { 140 switch packManifestVersion { 141 case PackManifestVersion1_0: 142 return packManifestV1_0(ctx, pusher, artifactType, opts) 143 case PackManifestVersion1_1: 144 return packManifestV1_1(ctx, pusher, artifactType, opts) 145 default: 146 return ocispec.Descriptor{}, fmt.Errorf("PackManifestVersion(%v): %w", packManifestVersion, errdef.ErrUnsupported) 147 } 148 } 149 150 // PackOptions contains optional parameters for [Pack]. 151 // 152 // Deprecated: This type is deprecated and not recommended for future use. 153 // Use [PackManifestOptions] instead. 154 type PackOptions struct { 155 // Subject is the subject of the manifest. 156 Subject *ocispec.Descriptor 157 158 // ManifestAnnotations is the annotation map of the manifest. 159 ManifestAnnotations map[string]string 160 161 // PackImageManifest controls whether to pack an OCI Image Manifest or not. 162 // - If true, pack an OCI Image Manifest. 163 // - If false, pack an OCI Artifact Manifest (deprecated). 164 // 165 // Default value: false. 166 PackImageManifest bool 167 168 // ConfigDescriptor is a pointer to the descriptor of the config blob. 169 // If not nil, artifactType will be implied by the mediaType of the 170 // specified ConfigDescriptor, and ConfigAnnotations will be ignored. 171 // This option is valid only when PackImageManifest is true. 172 ConfigDescriptor *ocispec.Descriptor 173 174 // ConfigAnnotations is the annotation map of the config descriptor. 175 // This option is valid only when PackImageManifest is true 176 // and ConfigDescriptor is nil. 177 ConfigAnnotations map[string]string 178 } 179 180 // Pack packs the given blobs, generates a manifest for the pack, 181 // and pushes it to a content storage. 182 // 183 // When opts.PackImageManifest is true, artifactType will be used as the 184 // the config descriptor mediaType of the image manifest. 185 // 186 // If succeeded, returns a descriptor of the manifest. 187 // 188 // Deprecated: This method is deprecated and not recommended for future use. 189 // Use [PackManifest] instead. 190 func Pack(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { 191 if opts.PackImageManifest { 192 return packManifestV1_1_RC2(ctx, pusher, artifactType, blobs, opts) 193 } 194 return packArtifact(ctx, pusher, artifactType, blobs, opts) 195 } 196 197 // packArtifact packs an Artifact manifest as defined in image-spec v1.1.0-rc2. 198 // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md 199 func packArtifact(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { 200 if artifactType == "" { 201 artifactType = MediaTypeUnknownArtifact 202 } 203 204 annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, spec.AnnotationArtifactCreated) 205 if err != nil { 206 return ocispec.Descriptor{}, err 207 } 208 manifest := spec.Artifact{ 209 MediaType: spec.MediaTypeArtifactManifest, 210 ArtifactType: artifactType, 211 Blobs: blobs, 212 Subject: opts.Subject, 213 Annotations: annotations, 214 } 215 return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations) 216 } 217 218 // packManifestV1_0 packs an image manifest defined in image-spec v1.0.2. 219 // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md 220 func packManifestV1_0(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { 221 if opts.Subject != nil { 222 return ocispec.Descriptor{}, fmt.Errorf("subject is not supported for manifest version %v: %w", PackManifestVersion1_0, errdef.ErrUnsupported) 223 } 224 225 // prepare config 226 var configDesc ocispec.Descriptor 227 if opts.ConfigDescriptor != nil { 228 if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil { 229 return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err) 230 } 231 configDesc = *opts.ConfigDescriptor 232 } else { 233 if artifactType == "" { 234 artifactType = MediaTypeUnknownConfig 235 } else if err := validateMediaType(artifactType); err != nil { 236 return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err) 237 } 238 var err error 239 configDesc, err = pushCustomEmptyConfig(ctx, pusher, artifactType, opts.ConfigAnnotations) 240 if err != nil { 241 return ocispec.Descriptor{}, err 242 } 243 } 244 245 annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) 246 if err != nil { 247 return ocispec.Descriptor{}, err 248 } 249 if opts.Layers == nil { 250 opts.Layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs 251 } 252 manifest := ocispec.Manifest{ 253 Versioned: specs.Versioned{ 254 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 255 }, 256 Config: configDesc, 257 MediaType: ocispec.MediaTypeImageManifest, 258 Layers: opts.Layers, 259 Annotations: annotations, 260 } 261 return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations) 262 } 263 264 // packManifestV1_1_RC2 packs an image manifest as defined in image-spec 265 // v1.1.0-rc2. 266 // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md 267 func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { 268 if configMediaType == "" { 269 configMediaType = MediaTypeUnknownConfig 270 } 271 272 // prepare config 273 var configDesc ocispec.Descriptor 274 if opts.ConfigDescriptor != nil { 275 configDesc = *opts.ConfigDescriptor 276 } else { 277 var err error 278 configDesc, err = pushCustomEmptyConfig(ctx, pusher, configMediaType, opts.ConfigAnnotations) 279 if err != nil { 280 return ocispec.Descriptor{}, err 281 } 282 } 283 284 annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) 285 if err != nil { 286 return ocispec.Descriptor{}, err 287 } 288 if layers == nil { 289 layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs 290 } 291 manifest := ocispec.Manifest{ 292 Versioned: specs.Versioned{ 293 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 294 }, 295 Config: configDesc, 296 MediaType: ocispec.MediaTypeImageManifest, 297 Layers: layers, 298 Subject: opts.Subject, 299 Annotations: annotations, 300 } 301 return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations) 302 } 303 304 // packManifestV1_1 packs an image manifest defined in image-spec v1.1.0. 305 // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#guidelines-for-artifact-usage 306 func packManifestV1_1(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { 307 if artifactType == "" && (opts.ConfigDescriptor == nil || opts.ConfigDescriptor.MediaType == ocispec.MediaTypeEmptyJSON) { 308 // artifactType MUST be set when config.mediaType is set to the empty value 309 return ocispec.Descriptor{}, ErrMissingArtifactType 310 } 311 if artifactType != "" { 312 if err := validateMediaType(artifactType); err != nil { 313 return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err) 314 } 315 } 316 317 // prepare config 318 var emptyBlobExists bool 319 var configDesc ocispec.Descriptor 320 if opts.ConfigDescriptor != nil { 321 if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil { 322 return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err) 323 } 324 configDesc = *opts.ConfigDescriptor 325 } else { 326 // use the empty descriptor for config 327 configDesc = ocispec.DescriptorEmptyJSON 328 configDesc.Annotations = opts.ConfigAnnotations 329 configBytes := ocispec.DescriptorEmptyJSON.Data 330 // push config 331 if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil { 332 return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) 333 } 334 emptyBlobExists = true 335 } 336 337 annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) 338 if err != nil { 339 return ocispec.Descriptor{}, err 340 } 341 if len(opts.Layers) == 0 { 342 // use the empty descriptor as the single layer 343 layerDesc := ocispec.DescriptorEmptyJSON 344 layerData := ocispec.DescriptorEmptyJSON.Data 345 if !emptyBlobExists { 346 if err := pushIfNotExist(ctx, pusher, layerDesc, layerData); err != nil { 347 return ocispec.Descriptor{}, fmt.Errorf("failed to push layer: %w", err) 348 } 349 } 350 opts.Layers = []ocispec.Descriptor{layerDesc} 351 } 352 353 manifest := ocispec.Manifest{ 354 Versioned: specs.Versioned{ 355 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 356 }, 357 Config: configDesc, 358 MediaType: ocispec.MediaTypeImageManifest, 359 Layers: opts.Layers, 360 Subject: opts.Subject, 361 ArtifactType: artifactType, 362 Annotations: annotations, 363 } 364 return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations) 365 } 366 367 // pushIfNotExist pushes data described by desc if it does not exist in the 368 // target. 369 func pushIfNotExist(ctx context.Context, pusher content.Pusher, desc ocispec.Descriptor, data []byte) error { 370 if ros, ok := pusher.(content.ReadOnlyStorage); ok { 371 exists, err := ros.Exists(ctx, desc) 372 if err != nil { 373 return fmt.Errorf("failed to check existence: %s: %s: %w", desc.Digest.String(), desc.MediaType, err) 374 } 375 if exists { 376 return nil 377 } 378 } 379 380 if err := pusher.Push(ctx, desc, bytes.NewReader(data)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { 381 return fmt.Errorf("failed to push: %s: %s: %w", desc.Digest.String(), desc.MediaType, err) 382 } 383 return nil 384 } 385 386 // pushManifest marshals manifest into JSON bytes and pushes it. 387 func pushManifest(ctx context.Context, pusher content.Pusher, manifest any, mediaType string, artifactType string, annotations map[string]string) (ocispec.Descriptor, error) { 388 manifestJSON, err := json.Marshal(manifest) 389 if err != nil { 390 return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err) 391 } 392 manifestDesc := content.NewDescriptorFromBytes(mediaType, manifestJSON) 393 // populate ArtifactType and Annotations of the manifest into manifestDesc 394 manifestDesc.ArtifactType = artifactType 395 manifestDesc.Annotations = annotations 396 // push manifest 397 if err := pusher.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { 398 return ocispec.Descriptor{}, fmt.Errorf("failed to push manifest: %w", err) 399 } 400 return manifestDesc, nil 401 } 402 403 // pushCustomEmptyConfig generates and pushes an empty config blob. 404 func pushCustomEmptyConfig(ctx context.Context, pusher content.Pusher, mediaType string, annotations map[string]string) (ocispec.Descriptor, error) { 405 // Use an empty JSON object here, because some registries may not accept 406 // empty config blob. 407 // As of September 2022, GAR is known to return 400 on empty blob upload. 408 // See https://github.com/oras-project/oras-go/issues/294 for details. 409 configBytes := []byte("{}") 410 configDesc := content.NewDescriptorFromBytes(mediaType, configBytes) 411 configDesc.Annotations = annotations 412 // push config 413 if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil { 414 return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) 415 } 416 return configDesc, nil 417 } 418 419 // ensureAnnotationCreated ensures that annotationCreatedKey is in annotations, 420 // and that its value conforms to RFC 3339. Otherwise returns a new annotation 421 // map with annotationCreatedKey created. 422 func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey string) (map[string]string, error) { 423 if createdTime, ok := annotations[annotationCreatedKey]; ok { 424 // if annotationCreatedKey is provided, validate its format 425 if _, err := time.Parse(time.RFC3339, createdTime); err != nil { 426 return nil, fmt.Errorf("%w: %v", ErrInvalidDateTimeFormat, err) 427 } 428 return annotations, nil 429 } 430 431 // copy the original annotation map 432 copied := make(map[string]string, len(annotations)+1) 433 maps.Copy(copied, annotations) 434 435 // set creation time in RFC 3339 format 436 // reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/annotations.md#pre-defined-annotation-keys 437 now := time.Now().UTC() 438 copied[annotationCreatedKey] = now.Format(time.RFC3339) 439 return copied, nil 440 } 441 442 // validateMediaType validates the format of mediaType. 443 func validateMediaType(mediaType string) error { 444 if !mediaTypeRegexp.MatchString(mediaType) { 445 return fmt.Errorf("%s: %w", mediaType, errdef.ErrInvalidMediaType) 446 } 447 return nil 448 }