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  }