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: pack OCI image spec v1.1 manifests #1054

Merged
merged 27 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from 23 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
30 changes: 17 additions & 13 deletions cmd/oras/internal/option/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,39 @@ package option
import (
"fmt"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/pflag"
"oras.land/oras-go/v2"
)

const (
ImageSpecV1_1 = "v1.1"
ImageSpecV1_0 = "v1.0"
// TODO: pending on https://github.com/oras-project/oras-go/issues/568
PackManifestTypeImageV1_0 = 0
)

// ImageSpec option struct.
type ImageSpec struct {
// Manifest type for building artifact
ManifestMediaType string

// specFlag should be provided in form of `<version>-<manifest type>`
specFlag string
flag string
PackType oras.PackManifestType
}

// Parse parses flags into the option.
func (opts *ImageSpec) Parse() error {
switch opts.specFlag {
case "v1.1-image":
opts.ManifestMediaType = ocispec.MediaTypeImageManifest
case "v1.1-artifact":
opts.ManifestMediaType = ocispec.MediaTypeArtifactManifest
switch opts.flag {
case ImageSpecV1_1:
opts.PackType = oras.PackManifestTypeImageV1_1_0_RC4
case ImageSpecV1_0:
opts.PackType = PackManifestTypeImageV1_0
default:
return fmt.Errorf("unknown image specification flag: %q", opts.specFlag)
return fmt.Errorf("unknown image specification flag: %q", opts.flag)
}
return nil
}

// ApplyFlags applies flags to a command flag set.
func (opts *ImageSpec) ApplyFlags(fs *pflag.FlagSet) {
fs.StringVar(&opts.specFlag, "image-spec", "v1.1-image", "[Experimental] specify manifest type for building artifact. options: v1.1-image, v1.1-artifact")
fs.StringVar(&opts.flag, "image-spec", ImageSpecV1_1, fmt.Sprintf("[Experimental] specify manifest type for building artifact. options: %s, %s", ImageSpecV1_1, ImageSpecV1_0))
}

// distributionSpec option struct.
Expand Down
7 changes: 1 addition & 6 deletions cmd/oras/root/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import (
type attachOptions struct {
option.Common
option.Packer
option.ImageSpec
option.Target

artifactType string
Expand All @@ -52,10 +51,6 @@ func attachCmd() *cobra.Command {
Example - Attach file 'hi.txt' with type 'doc/example' to manifest 'hello:v1' in registry 'localhost:5000':
oras attach --artifact-type doc/example localhost:5000/hello:v1 hi.txt

Example - Attach file "hi.txt" with specific media type when building the manifest:
oras attach --artifact-type doc/example --image-spec v1.1-image localhost:5000/hello:v1 hi.txt # OCI image
oras attach --artifact-type doc/example --image-spec v1.1-artifact localhost:5000/hello:v1 hi.txt # OCI artifact

Example - Attach file "hi.txt" using a specific method for the Referrers API:
oras attach --artifact-type doc/example --distribution-spec v1.1-referrers-api localhost:5000/hello:v1 hi.txt # via API
oras attach --artifact-type doc/example --distribution-spec v1.1-referrers-tag localhost:5000/hello:v1 hi.txt # via tag scheme
Expand Down Expand Up @@ -134,7 +129,7 @@ func runAttach(ctx context.Context, opts attachOptions) error {
packOpts := oras.PackOptions{
Subject: &subject,
ManifestAnnotations: annotations[option.AnnotationManifest],
PackImageManifest: opts.ManifestMediaType == ocispec.MediaTypeImageManifest,
PackImageManifest: true,
}
pack := func() (ocispec.Descriptor, error) {
return oras.Pack(ctx, store, opts.artifactType, descs, packOpts)
Expand Down
5 changes: 4 additions & 1 deletion cmd/oras/root/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras/cmd/oras/internal/display"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/graph"
Expand Down Expand Up @@ -117,7 +118,9 @@ func runCopy(ctx context.Context, opts copyOptions) error {
committed := &sync.Map{}
extendedCopyOptions := oras.DefaultExtendedCopyOptions
extendedCopyOptions.Concurrency = opts.concurrency
extendedCopyOptions.FindPredecessors = graph.FindReferrerPredecessors
extendedCopyOptions.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
return graph.Referrers(ctx, src, desc, "")
}
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
extendedCopyOptions.PreCopy = display.StatusPrinter("Copying", opts.Verbose)
extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
Expand Down
27 changes: 11 additions & 16 deletions cmd/oras/root/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ Example - Push file "hi.txt" with config type "application/vnd.me.config":
Example - Push file "hi.txt" with the custom manifest config "config.json" of the custom media type "application/vnd.me.config":
oras push --config config.json:application/vnd.me.config localhost:5000/hello:v1 hi.txt

Example - Push file "hi.txt" with specific media type when building the manifest:
oras push --image-spec v1.1-image localhost:5000/hello:v1 hi.txt # OCI image
oras push --image-spec v1.1-artifact localhost:5000/hello:v1 hi.txt # OCI artifact

Example - Push file to the insecure registry:
oras push --insecure localhost:5000/hello:v1 hi.txt

Expand Down Expand Up @@ -102,15 +98,16 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t
opts.RawReference = refs[0]
opts.extraRefs = refs[1:]
opts.FileRefs = args[1:]
if opts.manifestConfigRef != "" {
if opts.artifactType != "" {
return errors.New("--artifact-type and --config cannot both be provided")
}
if opts.ManifestMediaType == ocispec.MediaTypeArtifactManifest {
return errors.New("cannot build an OCI artifact with manifest config")
}
if err := option.Parse(&opts); err != nil {
return err
}
if opts.ImageSpec.PackType == option.PackManifestTypeImageV1_0 && opts.manifestConfigRef != "" && opts.artifactType != "" {
return errors.New("--artifact-type and --config cannot both be provided for 1.0 OCI image")
}
return option.Parse(&opts)
if opts.PackType == oras.PackManifestTypeImageV1_1_0_RC4 && opts.manifestConfigRef == "" && opts.artifactType == "" {
opts.artifactType = oras.MediaTypeUnknownArtifact
}
qweeah marked this conversation as resolved.
Show resolved Hide resolved
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return runPush(cmd.Context(), opts)
Expand All @@ -135,6 +132,8 @@ func runPush(ctx context.Context, opts pushOptions) error {
packOpts := oras.PackOptions{
ConfigAnnotations: annotations[option.AnnotationConfig],
ManifestAnnotations: annotations[option.AnnotationManifest],
PackImageManifest: true,
PackManifestType: opts.ImageSpec.PackType,
}
store, err := file.New("")
if err != nil {
Expand All @@ -152,10 +151,6 @@ func runPush(ctx context.Context, opts pushOptions) error {
}
desc.Annotations = packOpts.ConfigAnnotations
packOpts.ConfigDescriptor = &desc
packOpts.PackImageManifest = true
}
if opts.ManifestMediaType == ocispec.MediaTypeImageManifest {
packOpts.PackImageManifest = true
}
descs, err := loadFiles(ctx, store, annotations, opts.FileRefs, opts.Verbose)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ go 1.20

require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0-rc2
github.com/opencontainers/image-spec v1.1.0-rc4
github.com/oras-project/oras-credentials-go v0.2.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
golang.org/x/term v0.11.0
gopkg.in/yaml.v3 v3.0.1
oras.land/oras-go/v2 v2.2.1-0.20230627113607-6b5bd4b4372b
oras.land/oras-go/v2 v2.2.1-0.20230807082644-bbe92af00542
)

require (
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/oras-project/oras-credentials-go v0.2.0 h1:BvWAXo0e5unWR6Hfxyb0K04mHNHreQz/Zclw6IzCYJo=
github.com/oras-project/oras-credentials-go v0.2.0/go.mod h1:JVdg7a5k7hzTrEeeouwag0aCv7OLrS77r7/6w3gVirU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -34,5 +34,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
oras.land/oras-go/v2 v2.2.1-0.20230627113607-6b5bd4b4372b h1:NYynybpqtG3lLTZMWNlrvUlcyGakCke57tg4TX6w2kA=
oras.land/oras-go/v2 v2.2.1-0.20230627113607-6b5bd4b4372b/go.mod h1:goptA58HogB/6sLN7KV6FgoiurcVxd5QBqv8wMVB0as=
oras.land/oras-go/v2 v2.2.1-0.20230807082644-bbe92af00542 h1:lK/VtXN9+dzSD3EMuCXu0D28OJadBxB9yUpaJBDIABM=
oras.land/oras-go/v2 v2.2.1-0.20230807082644-bbe92af00542/go.mod h1:GeAwLuC4G/JpNwkd+bSZ6SkDMGaaYglt6YK2WvZP7uQ=
95 changes: 63 additions & 32 deletions internal/graph/graph.go
qweeah marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ import (
"oras.land/oras/internal/docker"
)

// MediaTypeArtifactManifest specifies the media type for a content descriptor.
const MediaTypeArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json"

// Artifact describes an artifact manifest.
// This structure provides `application/vnd.oci.artifact.manifest.v1+json` mediatype when marshalled to JSON.
//
// This manifest type was introduced in image-spec v1.1.0-rc1 and was removed in
// image-spec v1.1.0-rc3. It is not part of the current image-spec and is kept
// here for Go compatibility.
//
// Reference: https://github.com/opencontainers/image-spec/pull/999
type Artifact struct {
// MediaType is the media type of the object this schema refers to.
MediaType string `json:"mediaType"`

// ArtifactType is the IANA media type of the artifact this schema refers to.
ArtifactType string `json:"artifactType"`

// Blobs is a collection of blobs referenced by this manifest.
Blobs []ocispec.Descriptor `json:"blobs,omitempty"`

// Subject (reference) is an optional link from the artifact to another manifest forming an association between the artifact and the other manifest.
Subject *ocispec.Descriptor `json:"subject,omitempty"`

// Annotations contains arbitrary metadata for the artifact manifest.
Annotations map[string]string `json:"annotations,omitempty"`
}

// Successors returns the nodes directly pointed by the current node, picking
qweeah marked this conversation as resolved.
Show resolved Hide resolved
// out subject and config descriptor if applicable.
// Returning nil when no subject and config found.
Expand All @@ -43,18 +71,31 @@ func Successors(ctx context.Context, fetcher content.Fetcher, node ocispec.Descr
nodes = manifest.Layers
subject = manifest.Subject
config = &manifest.Config
case ocispec.MediaTypeArtifactManifest:
case MediaTypeArtifactManifest:
var fetched []byte
fetched, err = content.FetchAll(ctx, fetcher, node)
if err != nil {
return
}
var manifest ocispec.Artifact
var manifest Artifact
if err = json.Unmarshal(fetched, &manifest); err != nil {
return
}
nodes = manifest.Blobs
subject = manifest.Subject

qweeah marked this conversation as resolved.
Show resolved Hide resolved
case ocispec.MediaTypeImageIndex:
var fetched []byte
fetched, err = content.FetchAll(ctx, fetcher, node)
if err != nil {
return
}
var index ocispec.Index
if err = json.Unmarshal(fetched, &index); err != nil {
return
}
nodes = index.Manifests
subject = index.Subject
default:
nodes, err = content.Successors(ctx, fetcher, node)
}
Expand Down Expand Up @@ -83,12 +124,12 @@ func Referrers(ctx context.Context, target content.ReadOnlyGraphStorage, desc oc
}
for _, node := range predecessors {
switch node.MediaType {
case ocispec.MediaTypeArtifactManifest:
case MediaTypeArtifactManifest:
fetched, err := fetchBytes(ctx, target, node)
if err != nil {
return nil, err
}
var artifact ocispec.Artifact
var artifact Artifact
if err := json.Unmarshal(fetched, &artifact); err != nil {
return nil, err
}
Expand All @@ -109,8 +150,25 @@ func Referrers(ctx context.Context, target content.ReadOnlyGraphStorage, desc oc
if image.Subject == nil || !content.Equal(*image.Subject, desc) {
continue
}
node.ArtifactType = image.Config.MediaType
node.ArtifactType = image.ArtifactType
if node.ArtifactType == "" {
node.ArtifactType = image.Config.MediaType
}
node.Annotations = image.Annotations
case ocispec.MediaTypeImageIndex:
fetched, err := fetchBytes(ctx, target, node)
if err != nil {
return nil, err
}
var index ocispec.Index
if err := json.Unmarshal(fetched, &index); err != nil {
return nil, err
}
if index.Subject == nil || !content.Equal(*index.Subject, desc) {
continue
}
node.ArtifactType = index.ArtifactType
node.Annotations = index.Annotations
default:
continue
}
qweeah marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -121,33 +179,6 @@ func Referrers(ctx context.Context, target content.ReadOnlyGraphStorage, desc oc
return results, nil
}

// FindReferrerPredecessors returns referrer nodes of desc in target.
func FindReferrerPredecessors(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
var results []ocispec.Descriptor
if repo, ok := src.(registry.ReferrerLister); ok {
// get referrers directly
err := repo.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error {
results = append(results, referrers...)
return nil
})
if err != nil {
return nil, err
}
return results, nil
}
predecessors, err := src.Predecessors(ctx, desc)
if err != nil {
return nil, err
}
for _, node := range predecessors {
switch node.MediaType {
case ocispec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest:
results = append(results, node)
}
}
return results, nil
}

func fetchBytes(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]byte, error) {
rc, err := fetcher.Fetch(ctx, desc)
if err != nil {
Expand Down
Loading