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