working commit
This commit is contained in:
Vendored
+448
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
Copyright The ORAS Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oras
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
specs "github.com/opencontainers/image-spec/specs-go"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
"oras.land/oras-go/v2/internal/spec"
|
||||
)
|
||||
|
||||
const (
|
||||
// MediaTypeUnknownConfig is the default config mediaType used
|
||||
// - for [Pack] when PackOptions.PackImageManifest is true and
|
||||
// PackOptions.ConfigDescriptor is not specified.
|
||||
// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0
|
||||
// and PackManifestOptions.ConfigDescriptor is not specified.
|
||||
MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"
|
||||
|
||||
// MediaTypeUnknownArtifact is the default artifactType used for [Pack]
|
||||
// when PackOptions.PackImageManifest is false and artifactType is
|
||||
// not specified.
|
||||
MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when
|
||||
// "org.opencontainers.artifact.created" or "org.opencontainers.image.created"
|
||||
// is provided, but its value is not in RFC 3339 format.
|
||||
// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6
|
||||
ErrInvalidDateTimeFormat = errors.New("invalid date and time format")
|
||||
|
||||
// ErrMissingArtifactType is returned by [PackManifest] when
|
||||
// packManifestVersion is PackManifestVersion1_1 and artifactType is
|
||||
// empty and the config media type is set to
|
||||
// "application/vnd.oci.empty.v1+json".
|
||||
ErrMissingArtifactType = errors.New("missing artifact type")
|
||||
)
|
||||
|
||||
// PackManifestVersion represents the manifest version used for [PackManifest].
|
||||
type PackManifestVersion int
|
||||
|
||||
const (
|
||||
// PackManifestVersion1_0 represents the OCI Image Manifest defined in
|
||||
// image-spec v1.0.2.
|
||||
// Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md
|
||||
PackManifestVersion1_0 PackManifestVersion = 1
|
||||
|
||||
// PackManifestVersion1_1_RC4 represents the OCI Image Manifest defined
|
||||
// in image-spec v1.1.0-rc4.
|
||||
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md
|
||||
//
|
||||
// Deprecated: This constant is deprecated and not recommended for future use.
|
||||
// Use [PackManifestVersion1_1] instead.
|
||||
PackManifestVersion1_1_RC4 PackManifestVersion = PackManifestVersion1_1
|
||||
|
||||
// PackManifestVersion1_1 represents the OCI Image Manifest defined in
|
||||
// image-spec v1.1.1.
|
||||
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.1/manifest.md
|
||||
PackManifestVersion1_1 PackManifestVersion = 2
|
||||
)
|
||||
|
||||
// PackManifestOptions contains optional parameters for [PackManifest].
|
||||
type PackManifestOptions struct {
|
||||
// Subject is the subject of the manifest.
|
||||
// This option is only valid when PackManifestVersion is
|
||||
// NOT PackManifestVersion1_0.
|
||||
Subject *ocispec.Descriptor
|
||||
|
||||
// Layers is the layers of the manifest.
|
||||
Layers []ocispec.Descriptor
|
||||
|
||||
// ManifestAnnotations is the annotation map of the manifest. In order to
|
||||
// make [PackManifest] reproducible, set the key ocispec.AnnotationCreated
|
||||
// (i.e. "org.opencontainers.image.created") to a fixed value. The value
|
||||
// must conform to RFC 3339.
|
||||
ManifestAnnotations map[string]string
|
||||
|
||||
// ConfigDescriptor is a pointer to the descriptor of the config blob.
|
||||
// If not nil, ConfigAnnotations will be ignored.
|
||||
ConfigDescriptor *ocispec.Descriptor
|
||||
|
||||
// ConfigAnnotations is the annotation map of the config descriptor.
|
||||
// This option is valid only when ConfigDescriptor is nil.
|
||||
ConfigAnnotations map[string]string
|
||||
}
|
||||
|
||||
// mediaTypeRegexp checks the format of media types.
|
||||
// References:
|
||||
// - https://github.com/opencontainers/image-spec/blob/v1.1.1/schema/defs-descriptor.json#L7
|
||||
// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2
|
||||
var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}$`)
|
||||
|
||||
// PackManifest generates an OCI Image Manifest based on the given parameters
|
||||
// and pushes the packed manifest to a content storage using pusher. The version
|
||||
// of the manifest to be packed is determined by packManifestVersion
|
||||
// (Recommended value: PackManifestVersion1_1).
|
||||
//
|
||||
// - If packManifestVersion is [PackManifestVersion1_1]:
|
||||
// artifactType MUST NOT be empty unless opts.ConfigDescriptor is specified.
|
||||
// - If packManifestVersion is [PackManifestVersion1_0]:
|
||||
// if opts.ConfigDescriptor is nil, artifactType will be used as the
|
||||
// config media type; if artifactType is empty,
|
||||
// "application/vnd.unknown.config.v1+json" will be used.
|
||||
// if opts.ConfigDescriptor is NOT nil, artifactType will be ignored.
|
||||
//
|
||||
// artifactType and opts.ConfigDescriptor.MediaType MUST comply with RFC 6838.
|
||||
//
|
||||
// Each time when PackManifest is called, if a time stamp is not specified, a new time
|
||||
// stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated
|
||||
// (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible,
|
||||
// set the key ocispec.AnnotationCreated to a fixed value in
|
||||
// opts.ManifestAnnotations. The value MUST conform to RFC 3339.
|
||||
//
|
||||
// If succeeded, returns a descriptor of the packed manifest.
|
||||
func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion PackManifestVersion, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
|
||||
switch packManifestVersion {
|
||||
case PackManifestVersion1_0:
|
||||
return packManifestV1_0(ctx, pusher, artifactType, opts)
|
||||
case PackManifestVersion1_1:
|
||||
return packManifestV1_1(ctx, pusher, artifactType, opts)
|
||||
default:
|
||||
return ocispec.Descriptor{}, fmt.Errorf("PackManifestVersion(%v): %w", packManifestVersion, errdef.ErrUnsupported)
|
||||
}
|
||||
}
|
||||
|
||||
// PackOptions contains optional parameters for [Pack].
|
||||
//
|
||||
// Deprecated: This type is deprecated and not recommended for future use.
|
||||
// Use [PackManifestOptions] instead.
|
||||
type PackOptions struct {
|
||||
// Subject is the subject of the manifest.
|
||||
Subject *ocispec.Descriptor
|
||||
|
||||
// ManifestAnnotations is the annotation map of the manifest.
|
||||
ManifestAnnotations map[string]string
|
||||
|
||||
// PackImageManifest controls whether to pack an OCI Image Manifest or not.
|
||||
// - If true, pack an OCI Image Manifest.
|
||||
// - If false, pack an OCI Artifact Manifest (deprecated).
|
||||
//
|
||||
// Default value: false.
|
||||
PackImageManifest bool
|
||||
|
||||
// ConfigDescriptor is a pointer to the descriptor of the config blob.
|
||||
// If not nil, artifactType will be implied by the mediaType of the
|
||||
// specified ConfigDescriptor, and ConfigAnnotations will be ignored.
|
||||
// This option is valid only when PackImageManifest is true.
|
||||
ConfigDescriptor *ocispec.Descriptor
|
||||
|
||||
// ConfigAnnotations is the annotation map of the config descriptor.
|
||||
// This option is valid only when PackImageManifest is true
|
||||
// and ConfigDescriptor is nil.
|
||||
ConfigAnnotations map[string]string
|
||||
}
|
||||
|
||||
// Pack packs the given blobs, generates a manifest for the pack,
|
||||
// and pushes it to a content storage.
|
||||
//
|
||||
// When opts.PackImageManifest is true, artifactType will be used as the
|
||||
// the config descriptor mediaType of the image manifest.
|
||||
//
|
||||
// If succeeded, returns a descriptor of the manifest.
|
||||
//
|
||||
// Deprecated: This method is deprecated and not recommended for future use.
|
||||
// Use [PackManifest] instead.
|
||||
func Pack(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
|
||||
if opts.PackImageManifest {
|
||||
return packManifestV1_1_RC2(ctx, pusher, artifactType, blobs, opts)
|
||||
}
|
||||
return packArtifact(ctx, pusher, artifactType, blobs, opts)
|
||||
}
|
||||
|
||||
// packArtifact packs an Artifact manifest as defined in image-spec v1.1.0-rc2.
|
||||
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md
|
||||
func packArtifact(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
|
||||
if artifactType == "" {
|
||||
artifactType = MediaTypeUnknownArtifact
|
||||
}
|
||||
|
||||
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, spec.AnnotationArtifactCreated)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
manifest := spec.Artifact{
|
||||
MediaType: spec.MediaTypeArtifactManifest,
|
||||
ArtifactType: artifactType,
|
||||
Blobs: blobs,
|
||||
Subject: opts.Subject,
|
||||
Annotations: annotations,
|
||||
}
|
||||
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations)
|
||||
}
|
||||
|
||||
// packManifestV1_0 packs an image manifest defined in image-spec v1.0.2.
|
||||
// Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md
|
||||
func packManifestV1_0(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
|
||||
if opts.Subject != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("subject is not supported for manifest version %v: %w", PackManifestVersion1_0, errdef.ErrUnsupported)
|
||||
}
|
||||
|
||||
// prepare config
|
||||
var configDesc ocispec.Descriptor
|
||||
if opts.ConfigDescriptor != nil {
|
||||
if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err)
|
||||
}
|
||||
configDesc = *opts.ConfigDescriptor
|
||||
} else {
|
||||
if artifactType == "" {
|
||||
artifactType = MediaTypeUnknownConfig
|
||||
} else if err := validateMediaType(artifactType); err != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err)
|
||||
}
|
||||
var err error
|
||||
configDesc, err = pushCustomEmptyConfig(ctx, pusher, artifactType, opts.ConfigAnnotations)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
}
|
||||
|
||||
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
if opts.Layers == nil {
|
||||
opts.Layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
|
||||
}
|
||||
manifest := ocispec.Manifest{
|
||||
Versioned: specs.Versioned{
|
||||
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
|
||||
},
|
||||
Config: configDesc,
|
||||
MediaType: ocispec.MediaTypeImageManifest,
|
||||
Layers: opts.Layers,
|
||||
Annotations: annotations,
|
||||
}
|
||||
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations)
|
||||
}
|
||||
|
||||
// packManifestV1_1_RC2 packs an image manifest as defined in image-spec
|
||||
// v1.1.0-rc2.
|
||||
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md
|
||||
func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
|
||||
if configMediaType == "" {
|
||||
configMediaType = MediaTypeUnknownConfig
|
||||
}
|
||||
|
||||
// prepare config
|
||||
var configDesc ocispec.Descriptor
|
||||
if opts.ConfigDescriptor != nil {
|
||||
configDesc = *opts.ConfigDescriptor
|
||||
} else {
|
||||
var err error
|
||||
configDesc, err = pushCustomEmptyConfig(ctx, pusher, configMediaType, opts.ConfigAnnotations)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
}
|
||||
|
||||
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
if layers == nil {
|
||||
layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
|
||||
}
|
||||
manifest := ocispec.Manifest{
|
||||
Versioned: specs.Versioned{
|
||||
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
|
||||
},
|
||||
Config: configDesc,
|
||||
MediaType: ocispec.MediaTypeImageManifest,
|
||||
Layers: layers,
|
||||
Subject: opts.Subject,
|
||||
Annotations: annotations,
|
||||
}
|
||||
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations)
|
||||
}
|
||||
|
||||
// packManifestV1_1 packs an image manifest defined in image-spec v1.1.1.
|
||||
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.1/manifest.md#guidelines-for-artifact-usage
|
||||
func packManifestV1_1(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
|
||||
if artifactType == "" && (opts.ConfigDescriptor == nil || opts.ConfigDescriptor.MediaType == ocispec.MediaTypeEmptyJSON) {
|
||||
// artifactType MUST be set when config.mediaType is set to the empty value
|
||||
return ocispec.Descriptor{}, ErrMissingArtifactType
|
||||
}
|
||||
if artifactType != "" {
|
||||
if err := validateMediaType(artifactType); err != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// prepare config
|
||||
var emptyBlobExists bool
|
||||
var configDesc ocispec.Descriptor
|
||||
if opts.ConfigDescriptor != nil {
|
||||
if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err)
|
||||
}
|
||||
configDesc = *opts.ConfigDescriptor
|
||||
} else {
|
||||
// use the empty descriptor for config
|
||||
configDesc = ocispec.DescriptorEmptyJSON
|
||||
configDesc.Annotations = opts.ConfigAnnotations
|
||||
configBytes := ocispec.DescriptorEmptyJSON.Data
|
||||
// push config
|
||||
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
|
||||
}
|
||||
emptyBlobExists = true
|
||||
}
|
||||
|
||||
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
if len(opts.Layers) == 0 {
|
||||
// use the empty descriptor as the single layer
|
||||
layerDesc := ocispec.DescriptorEmptyJSON
|
||||
layerData := ocispec.DescriptorEmptyJSON.Data
|
||||
if !emptyBlobExists {
|
||||
if err := pushIfNotExist(ctx, pusher, layerDesc, layerData); err != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("failed to push layer: %w", err)
|
||||
}
|
||||
}
|
||||
opts.Layers = []ocispec.Descriptor{layerDesc}
|
||||
}
|
||||
|
||||
manifest := ocispec.Manifest{
|
||||
Versioned: specs.Versioned{
|
||||
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
|
||||
},
|
||||
Config: configDesc,
|
||||
MediaType: ocispec.MediaTypeImageManifest,
|
||||
Layers: opts.Layers,
|
||||
Subject: opts.Subject,
|
||||
ArtifactType: artifactType,
|
||||
Annotations: annotations,
|
||||
}
|
||||
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations)
|
||||
}
|
||||
|
||||
// pushIfNotExist pushes data described by desc if it does not exist in the
|
||||
// target.
|
||||
func pushIfNotExist(ctx context.Context, pusher content.Pusher, desc ocispec.Descriptor, data []byte) error {
|
||||
if ros, ok := pusher.(content.ReadOnlyStorage); ok {
|
||||
exists, err := ros.Exists(ctx, desc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existence: %s: %s: %w", desc.Digest.String(), desc.MediaType, err)
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := pusher.Push(ctx, desc, bytes.NewReader(data)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
|
||||
return fmt.Errorf("failed to push: %s: %s: %w", desc.Digest.String(), desc.MediaType, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushManifest marshals manifest into JSON bytes and pushes it.
|
||||
func pushManifest(ctx context.Context, pusher content.Pusher, manifest any, mediaType string, artifactType string, annotations map[string]string) (ocispec.Descriptor, error) {
|
||||
manifestJSON, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err)
|
||||
}
|
||||
manifestDesc := content.NewDescriptorFromBytes(mediaType, manifestJSON)
|
||||
// populate ArtifactType and Annotations of the manifest into manifestDesc
|
||||
manifestDesc.ArtifactType = artifactType
|
||||
manifestDesc.Annotations = annotations
|
||||
// push manifest
|
||||
if err := pusher.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("failed to push manifest: %w", err)
|
||||
}
|
||||
return manifestDesc, nil
|
||||
}
|
||||
|
||||
// pushCustomEmptyConfig generates and pushes an empty config blob.
|
||||
func pushCustomEmptyConfig(ctx context.Context, pusher content.Pusher, mediaType string, annotations map[string]string) (ocispec.Descriptor, error) {
|
||||
// Use an empty JSON object here, because some registries may not accept
|
||||
// empty config blob.
|
||||
// As of September 2022, GAR is known to return 400 on empty blob upload.
|
||||
// See https://github.com/oras-project/oras-go/issues/294 for details.
|
||||
configBytes := []byte("{}")
|
||||
configDesc := content.NewDescriptorFromBytes(mediaType, configBytes)
|
||||
configDesc.Annotations = annotations
|
||||
// push config
|
||||
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
|
||||
}
|
||||
return configDesc, nil
|
||||
}
|
||||
|
||||
// ensureAnnotationCreated ensures that annotationCreatedKey is in annotations,
|
||||
// and that its value conforms to RFC 3339. Otherwise returns a new annotation
|
||||
// map with annotationCreatedKey created.
|
||||
func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey string) (map[string]string, error) {
|
||||
if createdTime, ok := annotations[annotationCreatedKey]; ok {
|
||||
// if annotationCreatedKey is provided, validate its format
|
||||
if _, err := time.Parse(time.RFC3339, createdTime); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidDateTimeFormat, err)
|
||||
}
|
||||
return annotations, nil
|
||||
}
|
||||
|
||||
// copy the original annotation map
|
||||
copied := make(map[string]string, len(annotations)+1)
|
||||
maps.Copy(copied, annotations)
|
||||
|
||||
// set creation time in RFC 3339 format
|
||||
// reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/annotations.md#pre-defined-annotation-keys
|
||||
now := time.Now().UTC()
|
||||
copied[annotationCreatedKey] = now.Format(time.RFC3339)
|
||||
return copied, nil
|
||||
}
|
||||
|
||||
// validateMediaType validates the format of mediaType.
|
||||
func validateMediaType(mediaType string) error {
|
||||
if !mediaTypeRegexp.MatchString(mediaType) {
|
||||
return fmt.Errorf("%s: %w", mediaType, errdef.ErrInvalidMediaType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user