Skip to content

Commit

Permalink
feat: support specifying distribution spec in discover, cp and manife…
Browse files Browse the repository at this point in the history
…st push commands (#757)

Resolves #756 

Signed-off-by: Billy Zha <jinzha1@microsoft.com>
  • Loading branch information
qweeah committed Jan 20, 2023
1 parent 45e96a7 commit 78b365b
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 47 deletions.
17 changes: 2 additions & 15 deletions cmd/oras/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"context"
"errors"
"fmt"
"reflect"
"strings"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
Expand All @@ -28,15 +27,13 @@ import (
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/file"
"oras.land/oras-go/v2/registry/remote"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
)

type attachOptions struct {
option.Common
option.Packer
option.ImageSpec
option.DistributionSpec
option.Target

artifactType string
Expand Down Expand Up @@ -85,9 +82,6 @@ Example - Attach file to the manifest tagged 'v1' in an OCI layout folder 'layou
if err := option.Parse(&opts); err != nil {
return err
}
if opts.Type == option.TargetTypeOCILayout && opts.DistributionSpec.ReferrersAPI != nil {
return errors.New("cannot enforce referrers API for image layout target")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -121,15 +115,8 @@ func runAttach(opts attachOptions) error {
if err != nil {
return err
}
if opts.Reference == "" {
return oerrors.NewErrInvalidReferenceStr(opts.Reference)
}
if opts.ReferrersAPI != nil {
if repo, ok := dst.(*remote.Repository); ok {
repo.SetReferrersCapability(*opts.ReferrersAPI)
} else {
return fmt.Errorf("unexpected type %s with target type %s", reflect.TypeOf(dst), opts.Type)
}
if err := opts.EnsureReferenceNotEmpty(); err != nil {
return err
}
subject, err := dst.Resolve(ctx, opts.Reference)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions cmd/oras/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ Example - Upload an artifact from an OCI layout tar archive:
Example - Copy an artifact and its referrers:
oras cp -r localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1
Example - Copy an artifact and referrers using specific methods for the Referrers API:
oras cp -r --from-distribution-spec v1.1-referrers-api --to-distribution-spec v1.1-referrers-tag \
localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1
Example - Copy certain platform of an artifact:
oras cp --platform linux/arm/v5 localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1
Expand All @@ -86,6 +90,7 @@ Example - Copy an artifact with multiple tags with concurrency tuned:
}
cmd.Flags().BoolVarP(&opts.recursive, "recursive", "r", false, "recursively copy the artifact and its referrer artifacts")
cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 3, "concurrency level")
opts.EnableDistributionSpecFlag()
option.ApplyFlags(&opts, cmd.Flags())
return cmd
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/oras/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ func discoverCmd() *cobra.Command {
Example - Discover direct referrers of manifest 'hello:v1' in registry 'localhost:5000':
oras discover localhost:5000/hello:v1
Example - Discover direct referrers via referrers API:
oras discover --distribution-spec v1.1-referrers-api localhost:5000/hello:v1
Example - Discover direct referrers via tag scheme:
oras discover --distribution-spec v1.1-referrers-tag localhost:5000/hello:v1
Example - Discover all the referrers of manifest 'hello:v1' in registry 'localhost:5000', displayed in a tree view:
oras discover -o tree localhost:5000/hello:v1
Expand All @@ -77,6 +83,7 @@ Example - Discover referrers with type 'test-artifact' of manifest 'hello:v1' in

cmd.Flags().StringVarP(&opts.artifactType, "artifact-type", "", "", "artifact type")
cmd.Flags().StringVarP(&opts.outputType, "output", "o", "table", "format in which to display referrers (table, json, or tree). tree format will also show indirect referrers")
opts.EnableDistributionSpecFlag()
option.ApplyFlags(&opts, cmd.Flags())
return cmd
}
Expand Down
29 changes: 24 additions & 5 deletions cmd/oras/internal/option/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,15 @@ type Remote struct {
PasswordFromStdin bool
Password string

resolveFlag []string
resolveDialContext func(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error)
resolveFlag []string
resolveDialContext func(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error)
applyDistributionSpec bool
distributionSpec distributionSpec
}

// EnableDistributionSpecFlag set distribution specification flag as applicable.
func (opts *Remote) EnableDistributionSpecFlag() {
opts.applyDistributionSpec = true
}

// ApplyFlags applies flags to a command flag set.
Expand All @@ -57,6 +64,13 @@ func (opts *Remote) ApplyFlags(fs *pflag.FlagSet) {
fs.BoolVarP(&opts.PasswordFromStdin, "password-stdin", "", false, "read password or identity token from stdin")
}

func applyPrefix(prefix, description string) (flagPrefix, notePrefix string) {
if prefix == "" {
return "", ""
}
return prefix + "-", description + " "
}

// ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string.
// Commonly used for non-unary remote targets.
func (opts *Remote) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) {
Expand All @@ -68,9 +82,11 @@ func (opts *Remote) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description
)
if prefix == "" {
shortUser, shortPassword = "u", "p"
} else {
flagPrefix = prefix + "-"
notePrefix = description + " "
}
flagPrefix, notePrefix = applyPrefix(prefix, description)

if opts.applyDistributionSpec {
opts.distributionSpec.ApplyFlagsWithPrefix(fs, prefix, description)
}
fs.StringVarP(&opts.Username, flagPrefix+"username", shortUser, "", notePrefix+"registry username")
fs.StringVarP(&opts.Password, flagPrefix+"password", shortPassword, "", notePrefix+"registry password or identity token")
Expand Down Expand Up @@ -254,6 +270,9 @@ func (opts *Remote) NewRepository(reference string, common Common) (repo *remote
if repo.Client, err = opts.authClient(hostname, common.Debug); err != nil {
return nil, err
}
if opts.distributionSpec.referrersAPI != nil {
repo.SetReferrersCapability(*opts.distributionSpec.referrersAPI)
}
return
}

Expand Down
26 changes: 14 additions & 12 deletions cmd/oras/internal/option/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ package option

import (
"fmt"
"github.com/spf13/pflag"

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

// ImageSpec option struct.
Expand Down Expand Up @@ -50,34 +51,35 @@ func (opts *ImageSpec) ApplyFlags(fs *pflag.FlagSet) {
fs.StringVar(&opts.specFlag, "image-spec", "", "specify manifest type for building artifact. options: v1.1-image, v1.1-artifact")
}

// DistributionSpec option struct.
type DistributionSpec struct {
// ReferrersAPI indicates the preference of the implementation of the Referrers API.
// distributionSpec option struct.
type distributionSpec struct {
// referrersAPI indicates the preference of the implementation of the Referrers API.
// Set to true for referrers API, false for referrers tag scheme, and nil for auto fallback.
ReferrersAPI *bool
referrersAPI *bool

// specFlag should be provided in form of`<version>-<api>-<option>`
specFlag string
}

// Parse parses flags into the option.
func (opts *DistributionSpec) Parse() error {
func (opts *distributionSpec) Parse() error {
switch opts.specFlag {
case "":
opts.ReferrersAPI = nil
opts.referrersAPI = nil
case "v1.1-referrers-tag":
isApi := false
opts.ReferrersAPI = &isApi
opts.referrersAPI = &isApi
case "v1.1-referrers-api":
isApi := true
opts.ReferrersAPI = &isApi
opts.referrersAPI = &isApi
default:
return fmt.Errorf("unknown image specification flag: %q", opts.specFlag)
}
return nil
}

// ApplyFlags applies flags to a command flag set.
func (opts *DistributionSpec) ApplyFlags(fs *pflag.FlagSet) {
fs.StringVar(&opts.specFlag, "distribution-spec", "", "set OCI distribution spec version and API option. options: v1.1-referrers-api, v1.1-referrers-tag")
// ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string.
func (opts *distributionSpec) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) {
flagPrefix, notePrefix := applyPrefix(prefix, description)
fs.StringVar(&opts.specFlag, flagPrefix+"distribution-spec", "", "set OCI distribution spec version and API option for "+notePrefix+"target. options: v1.1-referrers-api, v1.1-referrers-tag")
}
30 changes: 15 additions & 15 deletions cmd/oras/internal/option/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package option

import (
"context"
"errors"
"fmt"
"os"
"strings"
Expand All @@ -25,7 +26,7 @@ import (
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/oci"
"oras.land/oras-go/v2/registry"
"oras.land/oras/cmd/oras/internal/errors"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/fileref"
)

Expand Down Expand Up @@ -67,15 +68,8 @@ func (opts *Target) AnnotatedReference() string {
// Since there is only one target type besides the default `registry` type,
// the full form is not implemented until a new type comes in.
func (opts *Target) applyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) {
var (
flagPrefix string
noteSuffix string
)
if prefix != "" {
flagPrefix = prefix + "-"
noteSuffix = description + " "
}
fs.BoolVarP(&opts.isOCILayout, flagPrefix+"oci-layout", "", false, "Set "+noteSuffix+"target as an OCI image layout.")
flagPrefix, notePrefix := applyPrefix(prefix, description)
fs.BoolVarP(&opts.isOCILayout, flagPrefix+"oci-layout", "", false, "Set "+notePrefix+"target as an OCI image layout.")
}

// ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string.
Expand All @@ -90,6 +84,9 @@ func (opts *Target) Parse() error {
switch {
case opts.isOCILayout:
opts.Type = TargetTypeOCILayout
if opts.Remote.distributionSpec.referrersAPI != nil {
return errors.New("cannot enforce referrers API for image layout target")
}
default:
opts.Type = TargetTypeRemote
}
Expand Down Expand Up @@ -168,7 +165,7 @@ func (opts *Target) NewReadonlyTarget(ctx context.Context, common Common) (ReadO
// EnsureReferenceNotEmpty ensures whether the tag or digest is empty.
func (opts *Target) EnsureReferenceNotEmpty() error {
if opts.Reference == "" {
return errors.NewErrInvalidReferenceStr(opts.RawReference)
return oerrors.NewErrInvalidReferenceStr(opts.RawReference)
}
return nil
}
Expand All @@ -180,6 +177,12 @@ type BinaryTarget struct {
To Target
}

// EnableDistributionSpecFlag set distribution specification flag as applicable.
func (opts *BinaryTarget) EnableDistributionSpecFlag() {
opts.From.EnableDistributionSpecFlag()
opts.To.EnableDistributionSpecFlag()
}

// ApplyFlags applies flags to a command flag set fs.
func (opts *BinaryTarget) ApplyFlags(fs *pflag.FlagSet) {
opts.From.ApplyFlagsWithPrefix(fs, "from", "source")
Expand All @@ -188,8 +191,5 @@ func (opts *BinaryTarget) ApplyFlags(fs *pflag.FlagSet) {

// Parse parses user-provided flags and arguments into option struct.
func (opts *BinaryTarget) Parse() error {
if err := opts.From.Parse(); err != nil {
return err
}
return opts.To.Parse()
return Parse(opts)
}
5 changes: 5 additions & 0 deletions cmd/oras/manifest/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func pushCmd() *cobra.Command {
Example - Push a manifest to repository 'localhost:5000/hello' and tag with 'v1':
oras manifest push localhost:5000/hello:v1 manifest.json
Example - Push a manifest using a specific method for the Referrers API:
oras manifest push --distribution-spec v1.1-referrers-api localhost:5000/hello:v1 manifest.json
oras manifest push --distribution-spec v1.1-referrers-tag localhost:5000/hello:v1 manifest.json
Example - Push a manifest with content read from stdin:
oras manifest push localhost:5000/hello:v1 -
Expand Down Expand Up @@ -94,6 +98,7 @@ Example - Push a manifest to an OCI layout folder 'layout-dir' and tag with 'v1'
},
}

opts.EnableDistributionSpecFlag()
option.ApplyFlags(&opts, cmd.Flags())
cmd.Flags().StringVarP(&opts.mediaType, "media-type", "", "", "media type of manifest")
cmd.Flags().IntVarP(&opts.concurrency, "concurrency", "", 5, "concurrency level")
Expand Down

0 comments on commit 78b365b

Please sign in to comment.