Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a PackOption to support packing image manifests that conform to image-spec v1.1.0-rc4 #550

Merged
merged 24 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type Fetcher interface {
// Pusher pushes content.
type Pusher interface {
// Push pushes the content, matching the expected descriptor.
// Reader is perferred to Writer so that the suitable buffer size can be
// Reader is preferred to Writer so that the suitable buffer size can be
// chosen by the underlying implementation. Furthermore, the implementation
// can also do reflection on the Reader for more advanced I/O optimization.
Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error
Expand Down
218 changes: 173 additions & 45 deletions pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,55 +31,117 @@ import (
)

const (
// MediaTypeUnknownConfig is the default mediaType used when no
// config media type is specified.
// MediaTypeUnknownConfig is the default mediaType used for [Pack] when
// PackOptions.PackImageManifest is true and PackOptions.PackManifestType
// is PackManifestTypeImageV1_1_0_RC2 and PackOptions.ConfigDescriptor
// is not specified.
MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"
// MediaTypeUnknownArtifact is the default artifactType used when no
// artifact type is specified.

// MediaTypeUnknownArtifact is the default artifactType used for [Pack]
// when PackOptions.PackImageManifest is false and artifactType is
// not specified.
MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
)

// ErrInvalidDateTimeFormat is returned by Pack() when
// AnnotationArtifactCreated or AnnotationCreated is provided, but its value
// is not in RFC 3339 format.
// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6
var ErrInvalidDateTimeFormat = errors.New("invalid date and time format")
// PackManifestType represents the manifest type used for [Pack].
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
type PackManifestType int

const (
// PackManifestTypeImageV1_1_0_RC2 represents the OCI Image Manifest type
// defined in image-spec v1.1.0-rc2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md
//
// Deprecated: This type is deprecated and not recommended for future use.
// Use PackManifestTypeImageV1_1_0_RC4 instead.
PackManifestTypeImageV1_1_0_RC2 PackManifestType = 0

// PackManifestTypeImageV1_1_0_RC4 represents the OCI Image Manifest type
// defined since image-spec v1.1.0-rc4.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md
PackManifestTypeImageV1_1_0_RC4 PackManifestType = 1
)

var (
// ErrInvalidDateTimeFormat is returned by [Pack] when
// AnnotationArtifactCreated or AnnotationCreated 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 [Pack] when artifactType is not
// specified and the config media type is set to
// "application/vnd.oci.empty.v1+json".
ErrMissingArtifactType = errors.New("missing artifact type")
)

// PackOptions contains parameters for [oras.Pack].
// PackOptions contains parameters for [Pack].
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 image manifest or not.
// - If true, pack an image manifest; artifactType will be used as the
// the config descriptor mediaType of the image manifest.
// - If false, pack an artifact manifest.
// Default: false.
// 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.
// Recommended value: true (See DefaultPackOptions).
PackImageManifest bool
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved

// PackManifestType controls which type of manifest to pack.
// This option is valid only when PackImageManifest is true.
//
// Default value: PackManifestTypeImageV1_1_0_RC2 (deprecated).
// Recommended value: PackManifestTypeImageV1_1_0_RC4 (See DefaultPackOptions).
PackManifestType PackManifestType
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved

// 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
}

// DefaultPackOptions provides the default PackOptions.
// Note that the default options are subject to change in the future.
var DefaultPackOptions PackOptions = PackOptions{
PackImageManifest: true,
PackManifestType: PackManifestTypeImageV1_1_0_RC4,
}

// 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 opts.PackImageManifest is true and opts.PackManifestType is
// [PackManifestTypeImageV1_1_0_RC2],
// artifactType will be used as the the config media type of the image
// manifest when opts.ConfigDescriptor is not specified.
// - If opts.PackImageManifest is true and opts.PackManifestType is
// [PackManifestTypeImageV1_1_0_RC4],
// [ErrMissingArtifactType] will be returned when none of artifactType and
// opts.ConfigDescriptor is specified.
//
// If succeeded, returns a descriptor of the manifest.
func Pack(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if opts.PackImageManifest {
return packImage(ctx, pusher, artifactType, blobs, opts)
if !opts.PackImageManifest {
return packArtifact(ctx, pusher, artifactType, blobs, opts)
}

switch opts.PackManifestType {
case PackManifestTypeImageV1_1_0_RC2:
return packImageRC2(ctx, pusher, artifactType, blobs, opts)
case PackManifestTypeImageV1_1_0_RC4:
return packImageRC4(ctx, pusher, artifactType, blobs, opts)
default:
return ocispec.Descriptor{}, fmt.Errorf("PackManifestType(%v): %w", opts.PackManifestType, errdef.ErrUnsupported)
}
return packArtifact(ctx, pusher, artifactType, blobs, opts)
}

// packArtifact packs the given blobs, generates an artifact manifest for the
Expand All @@ -101,28 +163,14 @@ func packArtifact(ctx context.Context, pusher content.Pusher, artifactType strin
Subject: opts.Subject,
Annotations: annotations,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err)
}
manifestDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, manifestJSON)
// populate ArtifactType and Annotations of the manifest into manifestDesc
manifestDesc.ArtifactType = manifest.ArtifactType
manifestDesc.Annotations = manifest.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
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations)
}

// packImage packs the given blobs, generates an image manifest for the pack,
// and pushes it to a content storage. artifactType will be used as the config
// descriptor mediaType of the image manifest.
// packImageRC2 packs the given blobs, generates an image manifest for the
// pack, and pushes it to a content storage.
// If succeeded, returns a descriptor of the manifest.
func packImage(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md
func packImageRC2(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if configMediaType == "" {
configMediaType = MediaTypeUnknownConfig
}
Expand All @@ -139,7 +187,7 @@ func packImage(ctx context.Context, pusher content.Pusher, configMediaType strin
configDesc = content.NewDescriptorFromBytes(configMediaType, configBytes)
configDesc.Annotations = opts.ConfigAnnotations
// push config
if err := pusher.Push(ctx, configDesc, bytes.NewReader(configBytes)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
}
}
Expand All @@ -161,20 +209,100 @@ func packImage(ctx context.Context, pusher content.Pusher, configMediaType strin
Subject: opts.Subject,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations)
}

// packImageRC4 packs the given blobs, generates an image manifest for the pack,
// and pushes it to a content storage.
// If succeeded, returns a descriptor of the manifest.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md#guidelines-for-artifact-usage
func packImageRC4(ctx context.Context, pusher content.Pusher, artifactType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
var emptyBlobExists bool
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
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
}
if artifactType == "" {
if configDesc.MediaType == ocispec.MediaTypeEmptyJSON {
// artifactType MUST be set when config.mediaType is set to the empty value
return ocispec.Descriptor{}, ErrMissingArtifactType
}
artifactType = configDesc.MediaType
}

annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
if len(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)
}
}
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: 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(ocispec.MediaTypeImageManifest, manifestJSON)
manifestDesc := content.NewDescriptorFromBytes(mediaType, manifestJSON)
// populate ArtifactType and Annotations of the manifest into manifestDesc
manifestDesc.ArtifactType = manifest.Config.MediaType
manifestDesc.Annotations = manifest.Annotations

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
}

Expand Down
Loading