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  }