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 12 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
150 changes: 133 additions & 17 deletions pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,52 +34,102 @@ const (
// MediaTypeUnknownConfig is the default mediaType used when no
// config media type is specified.
MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"

// MediaTypeUnknownArtifact is the default artifactType used when no
// artifact type is 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 = int32
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved

const (
// PackManifestTypeImageManifestLegacy 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
PackManifestTypeImageManifestLegacy PackManifestType = iota

// PackManifestTypeImageManifest 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
PackManifestTypeImageManifest
)
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved

// PackOptions contains parameters for [oras.Pack].
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" (the default config).
ErrMissingArtifactType = errors.New("missing artifact type")
)
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved

// 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.
// - If true, pack an OCI Image Manifest
// - If false, pack an OCI Artifact Manifest
// Default: false.
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: PackManifestTypeImageManifestLegacy.
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.
var DefaultPackOptions PackOptions = PackOptions{
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
PackImageManifest: true,
PackManifestType: PackManifestTypeImageManifest,
}

// 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
// PackManifestTypeImageManifestLegacy (the default value),
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
// 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
// PackManifestTypeImageManifest, [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 {
if !opts.PackImageManifest {
return packArtifact(ctx, pusher, artifactType, blobs, opts)
}

switch opts.PackManifestType {
case PackManifestTypeImageManifestLegacy:
return packImageLegacy(ctx, pusher, artifactType, blobs, opts)
case PackManifestTypeImageManifest:
return packImage(ctx, pusher, artifactType, blobs, opts)
default:
return ocispec.Descriptor{}, fmt.Errorf("PackManifestType %v is unsupported: %w", opts.PackManifestType, errdef.ErrUnsupported)
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
}
return packArtifact(ctx, pusher, artifactType, blobs, opts)
}

// packArtifact packs the given blobs, generates an artifact manifest for the
Expand Down Expand Up @@ -118,11 +168,12 @@ func packArtifact(ctx context.Context, pusher content.Pusher, artifactType strin
return manifestDesc, nil
}

// 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.
// packImageLegacy 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.
// 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) {
func packImageLegacy(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if configMediaType == "" {
configMediaType = MediaTypeUnknownConfig
}
Expand Down Expand Up @@ -178,6 +229,71 @@ func packImage(ctx context.Context, pusher content.Pusher, configMediaType strin
return manifestDesc, nil
}

// packImage 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 packImage(ctx context.Context, pusher content.Pusher, artifactType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
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
configData := ocispec.DescriptorEmptyJSON.Data
if err := pusher.Push(ctx, configDesc, bytes.NewReader(configData)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
}
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
}
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 err := pusher.Push(ctx, layerDesc, bytes.NewReader(layerData)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return ocispec.Descriptor{}, fmt.Errorf("failed to push empty layer: %w", err)
}
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
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,
}
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)
// 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
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
}

// ensureAnnotationCreated ensures that annotationCreatedKey is in annotations,
// and that its value conforms to RFC 3339. Otherwise returns a new annotation
// map with annotationCreatedKey created.
Expand Down
Loading